Skip to main content

Runtime overview

Beacon currently has a goose payload adapter, but not automatic goose installer wiring. There are no goose-specific endpoint install, hooks install, or discovery status paths yet. Manual goose coverage can combine two collection paths:
  • A manually written goose hooks file that invokes beacon-hooks --platform goose subcommands.
  • goose’s own OTLP HTTP export pointed at the local Beacon collector.
The two paths 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 configured is partial.
This page documents the current manual adapter contract. Until installer and discovery support are added, use it as implementation guidance rather than a turnkey beacon endpoint install workflow.
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 manually enabling goose telemetry, make sure:
  • goose is installed, either the CLI or the desktop app.
  • beacon endpoint install has run for the endpoint generally 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. A future Beacon installer should own a directory rather than a file, and should not read or write 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. Beacon 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.
Today, manually configured goose hooks are not reported as a first-class installed harness by beacon endpoint discover or beacon endpoint hooks status.
.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 Beacon’s side. It costs nothing today, because goose is the only supported runtime that reads this directory.

Registered events

goose exposes twelve lifecycle events. Beacon 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; Beacon 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. Beacon’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. Beacon 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. Beacon 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 Beacon therefore answers with an empty object.

Manual plugin directory

Because goose loads every plugin it discovers, adding a Beacon-shaped plugin directory is additive by construction: your own plugins keep working, untouched. If you create the conventional beacon-endpoint directory by hand, keep it dedicated to Beacon hook commands and avoid mixing unrelated hooks into the same hooks/hooks.json. This mirrors the ownership boundary the eventual installer should enforce.

Collection path 2: OpenTelemetry export

goose ships a real OpenTelemetry exporter emitting OTel GenAI semantic-convention spans. Configure it manually by pointing goose’s own config.yaml at the local collector HTTP port:
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 stays yours

config.yaml is a file you write by hand — your provider, your model, your extensions, your permissions. Beacon does not currently rewrite this file for goose; add or update the OTLP keys yourself and keep whatever backup practice you use for manual goose configuration.

The file is not the whole answer

Three environment conditions leave the config file saying “local” while goose exports nothing, so check goose’s launch environment as well as the file: Beacon has no goose discovery or status check yet, so nothing reports these for you: check each variable yourself.

Discovery and status

goose events normalize to the goose harness when the manual paths are configured. Hook events carry harness.collection_method=hook because the events, payload shapes, and hooks schema are goose’s. 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. Run one session and check the log to confirm events are arriving.

Manual configuration support

Beacon does not yet create, status-check, or uninstall goose hook files. To test the adapter, write a goose hooks.json manually and point hook commands at the installed beacon-hooks binary with --platform goose. Use goose’s own configuration to enable OTLP HTTP export to http://127.0.0.1:4318. Commit .agents/plugins/beacon-endpoint/ only if you intentionally want the hooks to apply to everyone working in the repository, remembering that project scope replaces any user-scope plugin with the same name 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 after — fragments 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

Beacon 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 Beacon — 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 Beacon 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. Beacon never writes a derived cost into an event; beacon token-usage and the dashboard show a separately labeled list-price estimate instead (see Cost: reported, estimated, effective).
  • 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. Beacon 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. 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 does not check them for goose.
  • 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 Beacon discovers and configures each runtime.