Hosts and adapters
Guild Stack runs inside a host: the AI coding tool you already use. The 3.0.0 release was tested end to end on two, Claude Code and Codex, and ships release evidence for both. Fourteen more hosts are registered with honest, capability-by-capability support rows — never assumed.
Try it. The first line shows which host this run is bound to. The second renders your settings into each registered host's config shape, so you can see what every host would get:
/guild:config models inspect
/guild:config show --render
The Power layer covers the two integration surfaces you can see and tune: hooks and the MCP servers. The Architect layer covers how a host presents itself, the full capability matrix, and the degrade chain.
Hooks
Guild Stack registers 10 hook events with 24 entries in hooks/hooks.json.
Each entry runs one script. Most hooks record telemetry or inject context. A few
can block or ask. This page lists all of them.
Three rules hold for almost every hook:
- It exits 0 on its own failure. A hook crash does not wedge a prompt or end a session.
- It stays silent when there is no active run. Off a Guild Stack run, most hooks print nothing.
- Scripts compile to committed bundles under
hooks/dist/(andhooks/agent-team/dist/). You run Node, not a build.
The exceptions to "exits 0" are noted below: using-guild-bootstrap exits 2 when a
Guild Stack root fails closed, and the agent-team TaskCreated and TaskCompleted hooks can reject.
Check that hooks fire
After a restart, start a Guild Stack run and look for .guild/runs/<run-id>/logs/v1.4-events.jsonl.
It holds hook_event rows and, after tool use, tool_call rows. Older hosts may skip
newer events such as PreToolUse and PreCompact; the handlers fall through without
breaking the session. If the bootstrap card shows but no run files appear, run
/guild:maintain audit and read the installed hooks/hooks.json.
SessionStart (5 entries)
| Script | What it does |
|---|---|
bootstrap.sh | Prints a short Guild Stack status block and command list. Always exits 0. |
detect-guild-version.js | Detects a .guild/ tree. For a v1, mixed, or corrupt tree it surfaces a migrate, dry-run, or skip prompt. It never changes the tree. A v2 tree or no tree is silent. |
update-check.js | Reads a machine cache and prints a one-line update signal. It never touches the network on the session path. A stale cache is refreshed by a detached background run. defaults.update.mode sets notify (default), auto, or off. Dev installs are silent. |
using-guild-bootstrap.js | Injects the using-guild gateway as model context, so the model knows when to reach for Guild Stack. On a Guild Stack root it checks the storage layout and fails closed (exit 2) if plugin or compile outputs are missing. |
session-reanchor.js | After a compact or resume, re-injects the run's identity, phase, next gate, and lead posture. It fires only on source of compact or resume, only for an open run, and only in the lead's session. |
UserPromptSubmit (3 entries)
| Script | What it does |
|---|---|
check-skill-coverage.sh | First runs the lifecycle gate (see below); if it blocks, nothing else prints. Otherwise it may print a one-line nudge when your prompt names a domain with no shipped specialist template. It nudges at most once per session. |
capture-telemetry.js | Appends one event line for the prompt to the run's logs/v1.4-events.jsonl, and mirrors it to the legacy events.ndjson. |
run-trace-start.js | Emits the run_started marker once per run. Later prompts are no-ops. Silent if no run id is resolvable. |
PreToolUse (1 entry, matcher *)
| Script | What it does |
|---|---|
pre-tool-use.js | Writes a pre-call sidecar entry that PostToolUse joins to compute latency. It is also where the security checks live: capability-scope enforcement, the security-event log, and the MCP description hash check. A pinned MCP tool whose live description hash differs from its pin gets an ask decision. It also hosts the backend-degradation guard, the tier-dispatch guard, and a per-tool lifecycle bound. |
The MCP pin is described in MCP servers. The
defaults.dispatch.block_unmarked_lanes key makes the backend-degradation guard strict.
PostToolUse (4 entries, two matchers)
Matcher * runs two entries. Matcher Write|Edit runs two more.
| Matcher | Script | What it does |
|---|---|---|
* | capture-telemetry.js | Appends to the legacy events.ndjson mirror. It skips the canonical file for PostToolUse, so tool calls are not logged twice. |
* | post-tool-use.js | Pairs the call with its pre entry, computes latency_ms, and writes the tool_call event. It sweeps stale pre entries (over 5 minutes) as errors. It scrubs durable surfaces in place. It also drains the lead's queued write requests, but only for a verified single work-loop redirect or evolve-loop --apply command. |
Write|Edit | comms-format-lint.js | Lints the file just written for the handoff receipt format. Advisory by default. With GUILD_COMMS_FORMAT_ENFORCE=1 it blocks (exit 2) only on a receipt with no, duplicate, or invalid guild.handoff.v2 block. |
Write|Edit | gate-outcome-writer.js | When review.md or verify.md is written under a run, records a gate outcome in run.yaml. It writes nothing if the report is ambiguous. |
PreCompact (1 entry)
| Script | What it does |
|---|---|
pre-compact.js | Records a hook_event for the compaction. For an open run in the lead's session it prints plain-text instructions so the run's identity, phase, and next gate survive into the compact summary. It prints nothing without an open run. |
SubagentStop (2 entries)
| Script | What it does |
|---|---|
capture-telemetry.js | Appends a SubagentStop event with the specialist name. It runs first. |
maybe-reflect.js | A dev-team reflection branch. It acts only when GUILD_ENABLE_DEVTEAM_REFLECT=1, at least 3 subagent stops are logged, and a spec exists. Otherwise it is a silent no-op. |
Stop (5 entries)
Stop fires at the end of every assistant turn, not only at session end. The
entries run in this order.
| Script | What it does |
|---|---|
maybe-reflect.js | Runs the reflection gate. It passes only if a specialist was dispatched, a file was edited, and no error event exists. On pass it writes summary.md and prints GUILD_REFLECT run_id=<run-id> for the orchestrator. |
run-trace-close.js | Closes the active run and emits the run_closed marker. If a closed run sees later tool activity, it re-closes so provenance stays current. |
learning-backstop.js | Guarantees a guild.learning_checkpoint.v1 record exists for every phase in phases_log. It adds a no-op record, marked as a backstop, where the model forgot. It never overwrites an existing one. |
lean-lead-guard.js | The lean-lead advisory. See below. |
lifecycle-gate.js | The close-time lifecycle gate. See below. |
Agent-team events (3 entries)
These three belong to the experimental tmux agent-team backend. TaskCreated,
TaskCompleted, and TeammateIdle are gated on CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1.
Without it they pass through.
| Event | Script | What it does |
|---|---|---|
TaskCreated | task-created.js | Blocks a task with no owner specialist, no output contract, or a depends-on: reference to a task id that is in no plan file. |
TaskCompleted | task-completed.js | Validates the guild.handoff.v2 envelope, extracts learnings[] into the run, and checks the markdown receipt sections. It upserts the lane in run-state.json. It records a submission only. It does not dismiss the worker. |
TeammateIdle | teammate-idle.js | Nudges an idle teammate whose task has no receipt. It never exits non-zero. It reports a liveness verdict from the heartbeat file. |
The heartbeat timeout is tier-scaled: 180000 ms for cheap, 600000 for mid,
1200000 for powerful. An explicit defaults.heartbeat_timeout_ms always wins.
The two advisory guards
Both guards read the run's own telemetry. Neither bookkeeps anything new. Both are silent without an active run, and both fire once per threshold crossing, so your next prompt always goes through.
| Lean-lead guard | Lifecycle gate | |
|---|---|---|
| Question it asks | Is the lead doing lane work inline while lanes are still open? | Is the session doing any work outside the lifecycle? |
| Event | Stop | UserPromptSubmit (through check-skill-coverage.sh) and Stop |
| Counts | The lead's own Edit and Write calls with open lanes | The lead's own ad-hoc Bash, Edit, and Write calls since the last lifecycle touch |
| Anchor | Last compaction | Last lifecycle skill call or specialist dispatch |
| Effect | Advisory context. Never denies a tool call. | Blocks the prompt once, or at Stop blocks a build whose lanes finished with no review.md or verify.md |
| Enable key | defaults.lean_lead.enabled (default true) | defaults.lifecycle_gate.enabled (default true) |
| Threshold key | defaults.lean_lead.hands_on_edit_threshold (default 8) | defaults.lifecycle_gate.adhoc_activity_threshold (default 20) |
| Session override | GUILD_LEAN_LEAD_OVERRIDE=1 | GUILD_LIFECYCLE_GATE_OVERRIDE=1 |
| One-prompt override | none | [guild:gate-override] in the prompt |
A threshold below 1 or a non-integer is ignored and the default applies. After a
compaction the lean-lead guard fires on the first lead edit of the new cycle, then
returns to its every-N-edits cadence. The lifecycle gate delivers its correction to
the model on the next accepted prompt, because a block reaches you but not the model.
The close-time Stop gate re-arms only when the set of missing artifacts changes, under
a per-run fire cap.
Related config keys and env vars
| Name | Used by |
|---|---|
defaults.update.mode, defaults.update.cadence_hours | update-check.js |
defaults.dispatch.block_unmarked_lanes, GUILD_BLOCK_UNMARKED_LANES | pre-tool-use.js |
defaults.heartbeat_timeout_ms | teammate-idle.js |
GUILD_COMMS_FORMAT_ENFORCE=1 | comms-format-lint.js |
GUILD_ENABLE_DEVTEAM_REFLECT=1 | maybe-reflect.js |
CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1 | the three agent-team hooks |
See Config reference for every key.
MCP servers
Guild Stack ships two optional MCP servers. Both are read-only, both run over stdio, and Guild Stack works end to end without them. Together they give specialists ranked access to the wiki and a queryable view of past runs.
| Server | MCP id | Reads | Tools |
|---|---|---|---|
guild-memory | wiki | .guild/wiki/** | wiki_search, wiki_get, wiki_list |
guild-telemetry | trace | .guild/runs/** | trace_list_runs, trace_summary, trace_query, trace_cost_rollup |
Both are ids of one binary. The plugin starts them as
node ${CLAUDE_PLUGIN_ROOT}/runtime/guild-mcp.js wiki and
node ${CLAUDE_PLUGIN_ROOT}/runtime/guild-mcp.js trace. The binary is a committed
build, so you only need Node.
Try it
With the plugin loaded, ask the host to list your runs and then summarize one. The
host calls trace_list_runs, then trace_summary with a run_id from the result.
trace_list_runs is the place to start. trace_summary needs a run id; trace_query and trace_cost_rollup take an optional run_id and span all runs without it.
guild-memory tools
Every tool takes an optional cwd. See Root resolution.
| Tool | Parameters | Output |
|---|---|---|
wiki_search | query (string), category, limit, cwd | { results: [...] }. Each result has path, category, type, frontmatter_category, score, excerpt, confidence, source_refs. |
wiki_get | path (string, relative to the wiki root), cwd | { path, frontmatter, body }. A path outside the wiki root is rejected. |
wiki_list | category, updated_since (ISO-8601 date or datetime), cwd | Every page, sorted by path. Fields: path, category, type, frontmatter_category, title, updated, confidence. |
Search notes:
- Ranking is BM25. Title tokens count double. The title comes from frontmatter
title, else the first H1, else the filename. categoryis always the wiki directory segment, such asdecisions. A page's own frontmattercategoryis returned separately asfrontmatter_category.limitdefaults to 20 and is capped at 200.updated_sincereadsupdated_at(falling back toupdated). Pages with neither field are excluded when the filter is on.
guild-telemetry tools
| Tool | Parameters | Output |
|---|---|---|
trace_list_runs | since, limit (1 to 1000), cwd | { runs, total }. Each run has run_id, event_count, started_at, ended_at. since filters on ended_at, falling back to started_at. |
trace_summary | run_id (required), cwd | { run_id, source, summary }. source is file when a stored summary.md exists, or synthesized when it is rebuilt in memory with no write. |
trace_query | run_id, event, specialist, since, limit (1 to 10000), cwd | { events, total }. Each event is annotated with its run_id. Sorted by run_id, then ts. Omit run_id to search all runs. |
trace_cost_rollup | run_id, since, cwd | Run-wide totals plus by_tier, by_model, and by_specialist. Each row is { key, input, output, cached, total }, sorted by total descending. llm_event_count says how many events carried tokens. Omit run_id to roll up all runs. |
The server reads logs/v1.4-events.jsonl first and falls back to the legacy
events.ndjson. Fields are normalized so every tool works on both shapes. Malformed
lines are skipped.
Root resolution
Each call picks its root in this order. The first match wins.
| Step | guild-memory | guild-telemetry |
|---|---|---|
1. The cwd argument | <cwd>/.guild/wiki/ | <cwd>/.guild/runs/ |
| 2. An env var | GUILD_MEMORY_WIKI_ROOT | GUILD_TELEMETRY_CWD |
| 3. The server process cwd | <cwd>/.guild/wiki/ | <cwd>/.guild/runs/ |
The per-call cwd wins so that one long-lived server can answer for several
federated child repos. The env var is used mainly by tests, when no cwd is given.
--no-cwd-fallback
Some hosts launch the server from the plugin install, not your project. The
generated Codex manifest does this: it declares cwd: ".", which is the plugin
payload root, and passes --no-cwd-fallback. Step 3 would then serve Guild Stack's own
bundled data instead of yours. In this mode:
- Step 3 is removed. There is no process-cwd default.
- The per-call
cwdand the env var must be absolute paths. - A call with no usable root fails closed with
isError: trueand a message that says what to pass.
Without the flag, as in Claude Code and a dev checkout, behavior is unchanged.
Read-only, declared and checked
Both servers declare a capability block in .mcp.json:
| Field | guild-memory | guild-telemetry |
|---|---|---|
read_only | true | true |
read_paths | .guild/wiki/** | .guild/runs/** |
write_paths | none | none |
network | none | none |
The declaration is a definition-file field. The audit compares it with observed
behavior, and undeclared egress or writes are a violation. The read-only claim is also
checked mechanically: the sources contain no writeFile or appendFile calls. The
servers also detect the storage layout on start and never upgrade it. A future layout
fails closed.
Tool-description hash pinning
A compromised MCP server could change a tool's description after you trusted it.
Guild Stack guards against that by pinning a SHA-256 of each description. On every MCP tool
call, PreToolUse recomputes the live hash and compares it with the pin. A mismatch
returns an ask decision, so you confirm before the call proceeds.
Two kinds of pin exist:
- Shipped pins.
runtime/mcp-descriptions.pins.jsonholds a hash for each of the 7 Guild Stack tools. The compile step writes it together with the binary, so there is no re-pin ritual. Pins are keyed by server. A third-party server that names a toolwiki_searchdoes not inherit Guild Stack's pin. - Your pins.
mcp.tool_description_hashesmaps a tool name to its hash, for third-party tools. Re-pin with/guild:config update-mcp-hashes.
If a pinned tool's live description cannot be read, the result is "unverifiable". For a Guild Stack tool the verified binary stands in for the description and the call is allowed with a recorded event. If no such voucher applies, a Guild Stack tool asks. A third-party tool proceeds and the event is recorded.
See Config reference for mcp.* keys, and
Hooks for PreToolUse.
Host adapters
Guild Stack v3 runs on two live hosts: Claude Code and Codex. The v3.0.0 conformance evidence covers both, at 31/31 activated-host captures each.
The surface each host sees
The command surface folds from 22 to 13 active commands. Host-indexed skills drop from 110 to 17.
On stable 3.0.0, eleven print-only alias files remain. Each prints its new spelling and exits. The upcoming beta (next) releases delete them, leaving exactly the 13 dispatchers; every retired spelling keeps a sub-verb route on a surviving command.
| Claude Code | Codex | |
|---|---|---|
| Manifest | .claude-plugin/plugin.json | .codex-plugin/plugin.json |
| Skill path | not pinned in the matrix | .agents/skills/guild/** |
| Activation proof | launcher-declared beta receipt | probe on guild:guild-quality |
Both manifests belong to one version surface. You edit .claude-plugin/plugin.json and run npm run sync:claude-install. That regenerates every version-bearing file, the Codex manifest included.
Activation, per host
- Claude Code. The capture launcher declares
GUILD_ACTIVATION_CHANNEL=betafrom the attested release. The SessionStart hook mints a beta receipt only under that declaration. The verifier no longer accepts the retired check that guessed the channel from the version string alone. - Codex. The activation probe targets
guild:guild-qualityand tolerates exactly one trailing newline.
Source identity
On either host, an activated capture stages a blob-verified plugin-source twin at the attested commit (guild.plugin_source_identity.v1), and verify enforces it.
The host matrix
Guild Stack routes on capability columns, never on a host name. Every host has one registry row (guild.host_registry.v1) that embeds a capability row (guild.host_capabilities.v1). This page is the reference for both. For how the two live hosts present themselves, read Host adapters. For the rules an extension must meet, read Extension points.
The 16 host ids
Only two hosts are live-verified: claude-code-cli (Claude Code) and codex-cli (Codex). The other 14 are in INFERRED_HOSTS, which means at least one rung or column is not yet confirmed on a live host.
| Host id | Family | Surface | Installability | Result adapter | Dispatch selectable | Live-verified |
|---|---|---|---|---|---|---|
claude-code-cli | claude | cli | native | no | yes | yes |
codex-cli | codex | cli | target | yes | yes | yes |
pi-cli | pi | cli | target | no | yes | no |
antigravity-cli | antigravity | cli | target | no | yes | no |
cursor | cursor | cli | target | no | yes | no |
github-copilot | copilot | cli | target | no | yes | no |
opencode | opencode | cli | target | no | yes | no |
rovo-dev | rovo | cli | target | no | yes | no |
agents-file | agents | file | target | no | no | no |
kiro | agents | file | target | no | no | no |
qoder | agents | file | target | no | no | no |
trae | agents | file | target | no | no | no |
claude-code-app | claude | app | none | no | no | no |
claude-code-web | claude | app | none | no | no | no |
codex-app | codex | app | none | no | no | no |
claude-ai-connector | claude | app | none | no | no | no |
Three columns are independent. installability (native, target, none) says whether a package can be installed. result_adapter says whether a cross-review adapter exists. dispatch_selectable says whether a lane can run there. A host can be dispatch-selectable without a result adapter.
agents-file is a render target, not a launcher. Its adapter's dispatch returns degraded with no command. kiro, qoder and trae bind to it (adapter_binding: "agents-file") and share the agents family. trae-cn is not a separate id; it folds into trae. Legacy aliases normalize into this set and are never authoritative ids.
The registry also carries a per-row provenance of verified or inferred. Separately, six rows read verified for their columns: the two live hosts, pi-cli, antigravity-cli, github-copilot and opencode. For pi-cli and antigravity-cli the source notes "3 columns + detection live-checked" with the browser rung still inferred. Use INFERRED_HOSTS as the live-verification line.
Capability rows
A guild.host_capabilities.v1 row has these blocks, after host_kind, family and surface_kind:
| Block | What it records |
|---|---|
package | installable, installability (verified or target), manifest_format, update behavior. A host with no apply path carries apply: "none" and degrades to notify. In the upcoming beta (next) releases, every registered host's update row is pinned and exercised end to end against simulated registries — command posture, auto gating, cache cadence and offline silence; live-host soak stays a separate, operator-run check. |
bootstrap, commands, skills, agents | How Guild Stack is loaded and how its surface renders. |
injection | Per-dispatch definition and skill-bundle injection facts; support is verified, target or absent. |
hooks | Booleans for ten events: session_start, user_prompt_submit, pre_tool_use, post_tool_use, stop, pre_compact, subagent_stop, task_created, task_completed, teammate_idle. |
permissions, dispatch | Permission modes and dispatch behavior. |
interaction, sessions, structured_output, artifacts | Prompting, resume, structured output, artifacts. |
tools | Per-tool strength: native, bridge, emulated or none. |
mcp, models | MCP transports and model tiers. |
validateHostCapabilitiesV1 validates a row. The shipped constants are CLAUDE_CAPABILITIES, CODEX_CAPABILITIES and AGENTS_FILE_CAPABILITIES. Rows for other hosts are derived from the registry. A probed runtime row is planned for .guild/hosts/<host-id>/capability.json; the source calls it a later phase.
The degrade chain
A capability a host cannot advertise degrades through a fixed ladder, strongest first:
native > wrapped > bridged > emulated > degraded
Loss rank runs from 0 (native) to 4 (degraded). The ladder is resolved per adapter surface: interaction, session, semantic_tool, browser. Examples from the shipped table:
| Surface | Claude Code | Codex | agents-file |
|---|---|---|---|
| interaction | native | wrapped | bridged |
| session | native | wrapped | emulated |
| browser | bridged | bridged | degraded |
Every resolution produces a guild.degradation_receipt.v1 with the surface, host, chosen rung, a degraded flag (true below native) and a reason. An unknown host resolves to degraded and records that. Guild Stack never assumes a capability. The four app-style hosts (claude-code-app, claude-code-web, codex-app, claude-ai-connector) are degraded on interaction, session and browser.
The INFERRED honesty gate
A rung or column that has not been checked on a live host is marked INFERRED in source. isHostInferred(host) is true for every host in INFERRED_HOSTS. A host leaves that set only when all its cells are live-verified. New-CLI hosts take a wrapped-CLI posture from a minimal inferred capability row: not installable, every uncertain block degraded. New IDE hosts copy the agents-file rungs. Do not present an inferred host as supported beyond its row.
config show --render
config show --render renders the resolved config and the phase-permission block into the native config shape of every registered host: all 16 HOST_IDS, keyed by host id: 8 CLI surfaces, 4 file surfaces (agents-file, kiro, qoder, trae), and 4 app/connector refuse hosts. It is read-only and reads capability columns from the registry, not host names. A field a host cannot express is stubbed and listed in _unsupported.
It fails closed on leaks:
- A value whose source is
workspace-localorproject-local(gitignored) is withheld from the shared render and recorded in_redactionswith reasonlocal-scope. - Every emitted string is scanned against the canonical secret patterns plus your
secrets_policy.redaction_patterns. A match becomes<SECRET-REDACTED:label>. - With the durable-artifact default of
closed, a leak setsok: falseandblocked: true, meaning a real shared write would be refused. The inspection command flagsBLOCKEDand still exits 0.
Adapter-author notes
- The contract version is
guild.host_adapter.v1.0.0. An adapter implements ten operations:capabilities,bootstrap,preflight,dispatch,collect,renderCommandSurface,renderPackage,renderPermissionDecision,resolveModelParams,memory. - Each call returns a
guild.host_adapter_receipt.v1with statusok,degradedorunavailable, a reason, and an optional degradation receipt. - The baseline adapter fails closed. It reports the registry profile and returns explicit degraded results, never launching a binary or claiming parity.
- A new pane-dispatchable host needs a
HostKindmember, aHOST_REGISTRY_ROWSrow, a bespokePaneAdapteror the genericWrappedCliPaneAdapter, and a capability row (host-types.ts). A host withadapter_binding: "agents-file"gets noHostKindand is never pane-dispatched. - Start the row as
inferred. Flip it only after a live-host check, and drop it fromINFERRED_HOSTSonly when every cell is verified. - Where a host lacks
pre_tool_use_ask, Guild Stack turns aPreToolUseaskinto adeny. See Security and trust.