Overview
Goose custom agents are Markdown files with YAML frontmatter. Project agents live in.agents/agents/, user agents in ~/.agents/agents/.
MCP servers are Goose extensions. Goose reads them from a single user-level
config.yaml, so Observal merges its extensions into that file and leaves every
other key — providers, models, global settings — untouched.
Session telemetry uses a Goose hook plugin. observal doctor patch --harness goose writes ~/.agents/plugins/observal/, containing a plugin.json manifest
and a hooks/hooks.json file. The hooks run
observal_cli.hooks.session_push --harness goose, which reads Goose’s SQLite
session store and pushes new records through Observal’s shared acknowledged
delivery pipeline.
Supported capabilities
Setup
1. Install the Observal CLI
2. Authenticate
3. Pull an agent into Goose
~/.agents/agents/{name}.md. To install into the current project:
~/.config/goose/config.yaml, because
that is the only file Goose reads extensions from.
4. Install or refresh the Goose hook plugin
5. Check what is installed
Config paths
User-scope locations follow Goose’s own resolution order:
$GOOSE_PATH_ROOTwhen set to an absolute path — config becomes$GOOSE_PATH_ROOT/config, data$GOOSE_PATH_ROOT/data, and skills, agents and plugins move to$GOOSE_PATH_ROOT/.agents/.$XDG_CONFIG_HOME/$XDG_DATA_HOMEwhen set.~/.config/gooseand~/.local/share/goose.
%APPDATA%\Block\goose\config\config.yaml and
%APPDATA%\Block\goose\data\sessions\sessions.db. Scanning, doctor, and
session reading resolve those locations automatically. observal agent pull
still writes the documented Unix paths, so Windows users should move the
generated config.yaml extensions into the %APPDATA% config by hand.
Agent profile format
name is required by Goose. model is emitted when the install resolves a
model from the Goose model catalog.
Extension format
Observal writes MCP servers as Goose extensions:stdio or streamable_http extensions and only
ever touches the entries it owns. observal scan additionally reports legacy
sse extensions. Goose’s builtin, platform and frontend extensions run
in-process and inline_python carries code rather than a command, so none of
them are reported or rewritten.
Hook plugin spec
~/.agents/plugins/observal/hooks/hooks.json:
matcher: Goose treats matcher as a regular
expression, so a bare "*" would be invalid and the rule would be skipped.
Goose runs every hook through sh -c on all platforms, passes the event JSON on
stdin, waits for the command, and enforces the rule’s timeout. The Observal
hook therefore only reads Goose’s database and appends to the local outbox; the
network upload is handed to a detached worker so a slow or unreachable Observal
server can never stall a Goose turn.
The hook writes nothing to stdout and never exits 2, so it can never block a
PreToolUse tool call or a Stop turn boundary. doctor patch preserves any
non-Observal rules you add to the same plugin.
Goose records each discovered plugin under a plugins map in config.yaml
keyed by its absolute path, so the plugin activates itself on the next session.
To disable the plugin without removing it, add it to disabledPlugins in
~/.config/goose/settings.json. To remove it entirely:
Hook payload coverage
Verified against goose 1.45.0:
Because most events omit
working_dir, Observal reads the session’s working
directory from sessions.working_dir instead, which keeps agent attribution and
layer hashing correct on every event.
Session push behavior
Goose stores sessions in SQLite rather than JSONL, so the CLI projects each session onto an append-only JSONL mirror at~/.observal/sessions/goose/<session_id>.jsonl. Everything downstream — the
durable outbox, acknowledged delivery, checkpoint recovery, and the final
SHA-256 audit — is the shared engine used by every other harness.
- A hook event resolves the Goose session id from the payload.
sessions.dbis opened through afile:...?mode=roURI with a busy timeout, so Observal never writes to, migrates, or checkpoints Goose’s database, and never blocks a Goose session that is writing concurrently.- Rows newer than the mirror’s cursor are appended as JSONL records. The mirror only grows, which keeps previously acknowledged byte offsets valid.
- The batch is spooled to the local outbox and a detached worker performs the upload, so the hook returns in milliseconds regardless of server health.
SessionEndappends asession_endboundary carrying the session’s token and cost totals, then finalizes delivery with an integrity hash.- Delegated (subagent) sessions linked by
parent_session_idare mirrored and delivered alongside their parent.
Goose content blocks are parsed into trace events:
text, thinking,
redactedThinking, toolRequest, toolResponse (including failures),
toolConfirmationRequest, and error.
Backfilling existing sessions
Caveats
The hook plugin is shared per machine. It is installed bydoctor patch,
not by each agent pull.
MCP extensions are user scope only. Goose reads extensions from one
config.yaml, so --scope project still writes extensions to the user config.
Agents and skills honour the requested scope. Goose rewrites that same file when
it registers plugins, and neither Goose nor Observal preserves YAML comments in
it.
Goose Desktop sessions finalize later. Only the goose CLI emits
SessionEnd. Desktop sessions are still captured turn by turn through Stop;
their final integrity audit runs from background recovery or
observal reconcile.
Legacy JSONL sessions are not imported. Goose imports pre-1.10 .jsonl
files into sessions.db on upgrade, and Observal reads the database, so those
sessions are covered. Leftover .jsonl files on disk are ignored.
Rewritten conversations replay. goose session --resume --edit rewrites a
session’s messages. The mirror is append-only, so the rewritten history is
appended rather than replacing what was already delivered.
Late writes land on the next event. If Goose persists a message after the
SessionEnd hook has read the database, that row is delivered by the next hook
event or by observal reconcile.
