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 goosesubcommands. - goose’s own OTLP HTTP export pointed at the local Beacon collector.
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.Prerequisites
Before manually enabling goose telemetry, make sure:- goose is installed, either the CLI or the desktop app.
beacon endpoint installhas 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.
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.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 acommand 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.
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 conventionalbeacon-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 ownconfig.yaml at the local
collector HTTP port:
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 thegoose 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 goosehooks.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 itsdeveloper 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’sedit 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:
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 setfield_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_outputfield 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_usdis empty. Beacon never writes a derived cost into an event;beacon token-usageand the dashboard show a separately labeled list-price estimate instead (see Cost: reported, estimated, effective). - Token usage needs the OTLP path. Usage arrives on
chatspans, so a hooks-only install reports none. That is expected rather than a fault, andbeacon token-usageclassifies goose accordingly. - No working directory on three events. goose omits it on
SessionStartandSessionEndwhen they come from the agent rather than the CLI session loop, and on aUserPromptSubmitraised by steering mid-turn. Those events carry nosession.working_directoryand no repository. Beacon deliberately does not fall back to the hook process’s own directory: goose runs hooks withsh -cinheriting 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,BeforeReadFileandAfterFileEditare deliberately not subscribed to. Each fires alongsidePreToolUseorPostToolUsefor 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.
PreToolUseResultreports what thePreToolUsehook 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/pluginsis 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:- Confirm the plugin is enabled. Check the
pluginsmap in~/.config/goose/config.yamlanddisabledPluginsin anysettings.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.
- Confirm
otel_exporter_otlp_endpointis in~/.config/goose/config.yamland names the collector’s HTTP port, not the gRPC one. - Check
OTEL_EXPORTER_OTLP_ENDPOINT,OTEL_SDK_DISABLEDandOTEL_EXPORTER_OTLP_PROTOCOLin 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.
Related
Supported agent harnesses
Return to the runtime support overview.
Agent harness integration model
See how Beacon discovers and configures each runtime.