Skip to main content

Runtime overview

Asymptote supports goose — the open-source agent from Block — through two collection paths, and both are wanted on the same machine:
--harness codename-goose and --harness block-goose are also accepted, and install the same thing. The two are not alternatives. goose’s hooks report prompts, tool calls, command lines and file edits; goose’s OpenTelemetry export reports token usage, the model and provider, response ids and the agent’s reasoning. Neither path carries the other’s data, so an endpoint with only one installed is half instrumented.
One integration covers the CLI and the desktop app. goose runs one agent core behind both, they discover plugins from the same directory, and both export under the same service.name. There is one harness name — goose — rather than one per surface.
GooseAI is a different product. GooseAI is an LLM inference service from another company, and gooseai, goose-ai and goose.ai are deliberately not accepted as harness names or install targets. They fall through as themselves, so a model provider can never be recorded as this runtime or have telemetry installed for it.

Prerequisites

Before enabling goose telemetry, make sure:
  • goose is installed, either the CLI or the desktop app.
  • beacon endpoint install has run so shared endpoint config, the collector, and runtime log paths exist. The OTLP path needs the collector actually listening; the hook path does not.
  • goose is restarted, or a new session started, after installing or removing hooks — plugins are discovered when a session begins.

Collection path 1: plugin hooks

goose discovers plugins as the immediate subdirectories of its plugins directory, names each plugin after its directory, and loads <plugin-root>/hooks/hooks.json from every plugin that is enabled. So Asymptote owns a directory rather than a file, and never reads or writes any other plugin beside it. $GOOSE_PATH_ROOT relocates the user-scope location when it is set to an absolute path, matching how goose resolves its own directories. A relative value is ignored, exactly as goose ignores it.
The two scopes replace each other; they do not add up. goose deduplicates plugins by name, taking project scope first — so a project-scope beacon-endpoint plugin replaces the user-scope one for sessions in that directory rather than running alongside it. Asymptote installs the same directory name at both scopes deliberately: an operator who installs at both gets one registration, not two copies of every event.This is the opposite of Kiro, where hook files merge across scopes, and it matches OpenHands, where a project file shadows the user one.
beacon endpoint inventory reports both scopes, so an install at either is visible.
.agents/plugins is a shared directory, not goose’s namespace. It is the Open Plugins convention, so another agent implementing that specification would also discover this plugin and run its hooks — and those events would be attributed to goose, because nothing in a hook payload says which host invoked it. goose sets no distinguishing variable and the event schema has no field for the question.This is a stated limitation with no fix on Asymptote’s side. It costs nothing today, because goose is the only supported runtime that reads this directory.

Registered events

goose exposes twelve lifecycle events. Asymptote registers seven, and the five it leaves out are covered under Known gaps.
goose hook timeouts are seconds, not milliseconds. The same timeout field is milliseconds on Qwen Code, in a hooks file that looks similar. goose’s own default is 30 seconds; Asymptote writes explicit values because these are ceilings on a hang, not budgets — and because goose holds the turn for a blocking hook, so the value on Stop is the ceiling on a stalled turn.
PostToolUse and PostToolUseFailure share the post-tool subcommand and are two separate registrations. goose has no single post-tool event: it sends one or the other, with otherwise identical payloads, and the event name is the only thing that distinguishes a write that landed from one that did not. Asymptote’s hooks carry no matcher. The field is a regex tested against goose’s matcher_context, and that is not a tool name on every event — on UserPromptSubmit it is the prompt text. A matcher that looked like a harmless .* would be a regex run over every prompt, and a narrower one would silently drop prompts that did not match it. Omitting it means always-match, which is what a telemetry hook wants on all seven.

The hook environment

goose runs a command action through sh -c, exports PLUGIN_ROOT, and sends the event as JSON on stdin. Asymptote passes the endpoint log and config locations to beacon-hooks as --log and --config flags rather than through the environment, so a hook finds them wherever it runs.
On goose, a hook’s stdout is judged, not ignored. PreToolUse and Stop run through goose’s blocking path, which classifies what the hook wrote: a hook that exits 0 with stdout carrying no recognized decision is classified as a failed hook. goose logs one per tool call and per turn, and on a rule configured on_failure: block it denies the tool call outright.The no-op {} that every other runtime reads as “no opinion” lands there, and so does {"permission":"allow"} — goose reads decision, and accepts only allow and block. Asymptote therefore answers {"decision":"allow"} on both blocking events.That allow approves nothing. goose’s hook chain is a plugin-policy layer inside ToolExecutionOperation, and the pipeline runs ToolApprovalOperation before it — so the operator’s decision has already been made by the time a hook is consulted. It means “this policy hook does not object”, and reaches no permission judge. This is the opposite of Qwen Code, where answering “allow” would disarm the user’s own prompts and Asymptote therefore answers with an empty object.

Asymptote owns one plugin directory and nothing else

Because goose loads every plugin it discovers, an install is additive by construction: your own plugins keep working, untouched, and Asymptote never parses them. The one thing Asymptote claims is the directory name beacon-endpoint. If its hooks/hooks.json already holds a hook Asymptote did not write, both install and uninstall refuse with an error naming the hook rather than overwriting or deleting it — move that hook into a plugin of its own and run the install again. A file goose itself could not parse is overwritten, since there are no live hooks in it to protect. Re-running the install rewrites the file rather than appending to it, so a repair does not end with each event registered twice. Uninstall removes the file and the plugin directory Asymptote created, because a plugin directory with no hooks file is still a plugin goose discovers and records in config.yaml. A directory you put something else into is left alone.

Collection path 2: OpenTelemetry export

goose ships a real OpenTelemetry exporter emitting OTel GenAI semantic-convention spans, and beacon endpoint install --harness goose points it at the local collector by writing two keys into goose’s own config.yaml:
goose exports OTLP over HTTP only. Its build includes the HTTP transport and not gRPC, so it takes the collector’s HTTP port where Claude Code, Codex CLI and Gemini CLI take gRPC. Pointing goose at 4317 fails silently: the exporter builds successfully and the telemetry never arrives.
This path is what carries gen_ai.usage.* — including cache reads and writes — along with gen_ai.request.model, gen_ai.provider.name, gen_ai.response.id, gen_ai.response.finish_reasons and the agent’s reasoning, on chat and execute_tool spans. None of that appears on any hook.

Your config.yaml is edited, not rewritten

config.yaml is a file you write by hand — your provider, your model, your extensions, your permissions. Asymptote changes two keys and preserves comments, key order, values, nesting and two-space indentation, and backs the file up before writing. A config Asymptote cannot parse is refused rather than overwritten. The one thing not preserved is blank lines between top-level keys, which the YAML library cannot retain. That is the reason for the backup.

The file is not the whole answer

Three environment conditions leave the config file saying “local” while goose exports nothing, so beacon endpoint status reads the environment as well as the file: Reporting “enabled” from the file alone would say a runtime is covered when it is silent, so each of these is reported as misconfigured with the variable named.

Discovery and status

Show goose status on both paths
goose reports as the goose harness. Hook events carry harness.collection_method=hook — the events, the payload shapes and the hooks schema are all goose’s, and Asymptote ships no plugin source goose loads and executes — while OTLP events carry harness.collection_method=otlp.
A hooks file on disk is a registration, not a guarantee. goose lets you disable a plugin from the plugins map in config.yaml or from disabledPlugins in a settings.json at any of three scopes, and a disabled plugin keeps its hooks file. beacon endpoint hooks status reports the registration, which is the narrower claim it can actually make — answering the rest would mean reimplementing goose’s precedence across four files it does not version. Run one session and check the log to confirm events are arriving.

Install or configuration support

Commit .agents/plugins/beacon-endpoint/ if you want the hooks to apply to everyone working in the repository — remembering that it then replaces any user-scope install for sessions in that repository.

Telemetry coverage

Tool taxonomy

goose registers its developer extension with unprefixed_tools, so its tools arrive bare — the model calls shell, not developer__shell. goose’s own resolver also recovers developer__shell and developer.shell from a model that adds the prefix anyway, so all three spellings reach a hook and all three classify identically.
MCP calls are identified by exclusion, and they have to be. goose calls an MCP tool <extension>__<tool> — no mcp anywhere in the name, no mcp_* argument, and no result to inspect, because goose populates no tool output at all. What identifies the call is that the prefix is not one of goose’s built-in extensions.Without that, a GitHub MCP server’s github__create_issue would be classified by the generic substring rules, which see “create” and would record an API call as a file modification.
read_image is the one tool whose argument is ambiguous: source is documented as a local path or an http(s) URL. A URL is never recorded under file.path, because that field is what every rule, git helper and SIEM query treats as a filesystem path.

Diffs, and the guard that makes them trustworthy

goose’s edit states the replaced span as before and afterfragments of the file, not copies of it, because the tool requires before to match the file exactly and uniquely. That is the same shape as Claude Code’s old_string/new_string, and it takes the same fragment-diff builder. write carries the whole new file under content and takes the whole-content builder. A failed edit is never recorded as a change. goose reports one with the same tool name and the same arguments as a success — its edit fails whenever before does not match — and the event name is the only thing that separates them. Without that guard, a diff would be built from an edit that never landed, and the log would assert a file changed when it did not. An edit whose before and after are identical produces no diff, for the same reason.

Approvals and enforcement

Asymptote records no approval decisions for goose, and does not synthesize any. goose genuinely does ask the operator — ToolApprovalOperation runs the permission judge, marks calls that need a person as not executable, and stops the turn for an answer — and exposes none of that to a hook. Its HookEvent set has no approval event at all, and PreToolUse fires identically whether the call was pre-approved by goose_mode, waved through, or already confirmed by somebody. An approval.allowed derived from it would claim a decision in exactly the cases where none was made. This is the same call already settled for Cline, Pi, OpenHands, Kiro, and fx. Enforcement stays behind the optional, off-by-default policy provider seam (BEACON_POLICY_PROVIDER), which is inert unless it names an executable and fails open on any error. goose is a runtime where a provider deny does take effect, and its deny shape is distinctive:
On goose the deny value is block, not deny. goose’s classifier accepts the literal strings allow and block and nothing else. deny — the word every other runtime’s deny shape uses — reads to goose as a hook that exited 0 without a verdict, which it treats as a failed hook and, on a rule not configured to block on failure, waves the tool call straight through. A seam that meant to deny would have allowed.
The reason rides along because goose puts it in front of both the operator and the model, prefixed with “Tool call denied by policy hook” and an instruction not to retry.

Data handling

goose content is handled like every other runtime. Prompts, tool arguments, command lines, paths, and diffs are retained in local or customer-controlled logs, with local secret redaction and per-string limits applied before writing, and a hash and byte count on each content-bearing event. Events over the 64 KiB limit drop raw and retained content, preserve stable metadata, and set field_truncated. The OTLP path carries message content only when OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT=true is set in goose’s environment. That is goose’s own opt-in and is not written by Asymptote — it cannot be: goose reads it from the environment only, never from config.yaml.

Known gaps

  • No tool output of any kind. goose’s hook context has a tool_output field and no emission site in the runtime populates it. So there are no command exit codes, no command output, no file-read contents and no MCP results anywhere on the hook path. A command is recorded as executed, with its command line, and nothing about how it went. This is the largest gap on this runtime, and closing it needs a change in goose.
  • No approval decisions. goose exposes no approval event, and Asymptote does not synthesize one from PreToolUse. Approval rules and dashboards will show nothing for this runtime.
  • No reported cost. goose reports token counts and not a price, so gen_ai.usage.cost_usd is empty. Asymptote does not derive cost from local pricing tables for any runtime.
  • Token usage needs the OTLP path. Usage arrives on chat spans, so a hooks-only install reports none. That is expected rather than a fault, and beacon token-usage classifies goose accordingly.
  • No working directory on three events. goose omits it on SessionStart and SessionEnd when they come from the agent rather than the CLI session loop, and on a UserPromptSubmit raised by steering mid-turn. Those events carry no session.working_directory and no repository. Asymptote deliberately does not fall back to the hook process’s own directory: goose runs hooks with sh -c inheriting its own cwd, which is the application bundle under the desktop app — a path that looks like a repository and resolves to nothing. Every tool event does carry it.
  • No duplicate shell and file events. BeforeShellExecution, AfterShellExecution, BeforeReadFile and AfterFileEdit are deliberately not subscribed to. Each fires alongside PreToolUse or PostToolUse for the same call, with the same tool name and arguments and — the part that makes them worse than redundant — no tool call id at all. Subscribing would record every shell command and file edit twice, with the second copy uncorrelatable.
  • No policy-decision events. PreToolUseResult reports what the PreToolUse hook chain decided, which plugin denied, and why. It is real signal with no duplicate elsewhere, but it fires once per tool call to report an outcome that is almost always “allow”, so subscribing as-is would double event volume. It is deferred rather than declined.
  • No compaction or subagent events. goose emits neither on a hook.
  • No transcript path. goose keeps conversation state in its own sessions database and no payload field points at it.
  • Another Open Plugins host would run these hooks. .agents/plugins is a shared convention directory, and nothing in a payload identifies the host. See Collection path 1.

Deployment notes

Plugins are discovered when a session starts, so an install taken while goose is running takes effect on the next session. Restart goose, or start a new session, after installing or removing hooks. Confirm status, then generate one goose event and check the log:
If goose hook telemetry is missing:
  • Confirm the plugin is enabled. Check the plugins map in ~/.config/goose/config.yaml and disabledPlugins in any settings.json — a disabled plugin keeps its hooks file, so the install looks complete.
  • Check $GOOSE_PATH_ROOT: if it is set to an absolute path, the user-scope plugin is under that directory, not ~/.agents.
  • Check for a project-scope install in the repository you are working in. It replaces the user-scope one rather than adding to it.
  • Confirm the file contains commands with --platform goose.
  • Start a new session so the plugin is rediscovered.
If goose token usage or model data is missing:
  • Confirm otel_exporter_otlp_endpoint is in ~/.config/goose/config.yaml and names the collector’s HTTP port, not the gRPC one.
  • Check OTEL_EXPORTER_OTLP_ENDPOINT, OTEL_SDK_DISABLED and OTEL_EXPORTER_OTLP_PROTOCOL in goose’s launch environment. Each can silently turn the export off or redirect it, and beacon endpoint status names whichever one is doing it.
  • Confirm the collector is running and listening on the HTTP port.

Supported agent harnesses

Return to the runtime support overview.

Agent harness integration model

See how Asymptote discovers and configures each runtime.