Externalize the hook routing table: derive from .agent-guard.yaml wraps: metadata #10

Open
opened 2026-05-23 20:53:18 +00:00 by coilysiren · 0 comments
Owner

Originally filed by @coilysiren on 2026-05-14T23:21:21Z - https://github.com/coilysiren/agent-guard/issues/13

Problem

agent-guard#10 ships the PreToolUse hook with two hardcoded routing tables baked into the Go binary: a coily-flavored one and an agent-guard-flavored one. This works for v0 but has two real problems:

  1. Drift. When coily grows a new coily ops <X> verb (or agent-guard grows a new agent-guard exec Makefile target), the routing table doesn't update. The wrapper inventory and the hint inventory live in different files, and the discipline that keeps them in sync is human attention.
  2. Closed-world. A third consumer of cli-guard (sibling cli-mcp, cli-web-docs, cli-web-ops) that wraps its own set of binaries cannot extend the table without forking agent-guard. The hook is generic in design but hardcoded in implementation.

Proposal

Externalize the routing table. Source of truth: the existing .agent-guard/agent-guard.yaml (or .coily/coily.yaml) that the consumer already maintains. Add a wraps: field per command:

commands:
  ops gh:
    run: <internal>
    description: Audited gh CLI passthrough.
    wraps: [gh]
  ops aws:
    run: <internal>
    description: Audited aws CLI passthrough.
    wraps: [aws]
  exec:
    run: <internal>
    description: Run a per-repo Makefile verb.
    wraps: [make, just, task, invoke]

The hook reads the discovered config at PreToolUse time, builds the routing table in memory (token -> "use <guard> <verb> ..."), and uses it instead of the hardcoded map. The recovery hint composes the guard binary name (from the discovered marker filename) with the verb declared in the YAML.

Why this is interesting

  • The wrapper repo's verb inventory becomes load-bearing for the routing-hint surface. The two cannot drift, because they are the same data.
  • Each consumer extends the table by editing their own YAML, no agent-guard fork required. cli-mcp can declare wraps: [npm, pnpm, yarn]; cli-web-ops can declare wraps: [terraform, helm]. Same hook binary, different tables.
  • The agent-guard lint discipline gets a third invariant: every wraps: entry's binary must not also be allowed by permissions.allow (otherwise the hook hint is irrelevant), and every recovery-hint-eligible binary in permissions.deny should appear in some wraps: (otherwise the agent gets the generic deny).
  • This is the substrate for solving coilysiren/coily#183 cleanly: once the table is in the YAML, the deny list can be auto-derived from it ("deny every bare invocation of every wraps: entry") which makes the trim-deny work mechanical, not manual.

Open design questions

  • Should wraps: carry an optional hint-extra (the GraphQL-trap-style notes)? Or should the hook hardcode those token-specific extras separately?
  • How does the hook handle the case where the same bare token appears in two consumers' tables (e.g. cwd is a nested repo)? Discovery picks the nearest, so this should be fine, but worth a test.
  • Should the hook honor a project-level override for hint language (e.g. a downstream wants its hint to read "use mywrapper foo" instead of "use agent-guard exec foo")? Yes - the guard name should come from the binary that wrote the YAML's lockdown, not from the marker filename.

Acceptance

  • .agent-guard/agent-guard.yaml / .coily/coily.yaml carry a wraps: field per command (where applicable). Existing entries without wraps: continue to work (no implied wrapping).
  • agent-guard hook pre-tool-use reads the config and routes from it. The hardcoded coilyRoutes / agentGuardRoutes maps are gone.
  • coily ships a coily.yaml with the canonical wraps: blocks for its ops/pkg/docker/ssh/tailscale verbs.
  • agent-guard lint enforces the two invariants above.

Followups (out of scope here)

  • coily#185 (replace lockdown-deny.sh with a call to this binary) consumes the externalized table.
  • A consumer that ships its own cli-guard binary (e.g. cli-mcp) can opt into this hook by declaring its own wraps:.

Notes

  • Pairs with agent-guard#10 (hook) and agent-guard#12 (install-hooks).
  • Sibling: coilysiren/coily#185.
_Originally filed by @coilysiren on 2026-05-14T23:21:21Z - [https://github.com/coilysiren/agent-guard/issues/13](https://github.com/coilysiren/agent-guard/issues/13)_ **Problem** agent-guard#10 ships the PreToolUse hook with two hardcoded routing tables baked into the Go binary: a coily-flavored one and an agent-guard-flavored one. This works for v0 but has two real problems: 1. **Drift.** When coily grows a new `coily ops <X>` verb (or agent-guard grows a new `agent-guard exec` Makefile target), the routing table doesn't update. The wrapper inventory and the hint inventory live in different files, and the discipline that keeps them in sync is human attention. 2. **Closed-world.** A third consumer of cli-guard (sibling `cli-mcp`, `cli-web-docs`, `cli-web-ops`) that wraps its own set of binaries cannot extend the table without forking agent-guard. The hook is generic in design but hardcoded in implementation. **Proposal** Externalize the routing table. Source of truth: the existing `.agent-guard/agent-guard.yaml` (or `.coily/coily.yaml`) that the consumer already maintains. Add a `wraps:` field per command: ```yaml commands: ops gh: run: <internal> description: Audited gh CLI passthrough. wraps: [gh] ops aws: run: <internal> description: Audited aws CLI passthrough. wraps: [aws] exec: run: <internal> description: Run a per-repo Makefile verb. wraps: [make, just, task, invoke] ``` The hook reads the discovered config at PreToolUse time, builds the routing table in memory (`token -> "use <guard> <verb> ..."`), and uses it instead of the hardcoded map. The recovery hint composes the guard binary name (from the discovered marker filename) with the verb declared in the YAML. **Why this is interesting** - The wrapper repo's verb inventory becomes load-bearing for the routing-hint surface. The two cannot drift, because they are the same data. - Each consumer extends the table by editing their own YAML, no agent-guard fork required. cli-mcp can declare `wraps: [npm, pnpm, yarn]`; cli-web-ops can declare `wraps: [terraform, helm]`. Same hook binary, different tables. - The `agent-guard lint` discipline gets a third invariant: every `wraps:` entry's binary must not also be allowed by `permissions.allow` (otherwise the hook hint is irrelevant), and every recovery-hint-eligible binary in `permissions.deny` should appear in some `wraps:` (otherwise the agent gets the generic deny). - This is the substrate for solving coilysiren/coily#183 cleanly: once the table is in the YAML, the deny list can be auto-derived from it ("deny every bare invocation of every `wraps:` entry") which makes the trim-deny work mechanical, not manual. **Open design questions** - Should `wraps:` carry an optional hint-extra (the GraphQL-trap-style notes)? Or should the hook hardcode those token-specific extras separately? - How does the hook handle the case where the same bare token appears in two consumers' tables (e.g. cwd is a nested repo)? Discovery picks the nearest, so this should be fine, but worth a test. - Should the hook honor a project-level override for hint *language* (e.g. a downstream wants its hint to read "use `mywrapper foo`" instead of "use `agent-guard exec foo`")? Yes - the guard name should come from the binary that wrote the YAML's lockdown, not from the marker filename. **Acceptance** - `.agent-guard/agent-guard.yaml` / `.coily/coily.yaml` carry a `wraps:` field per command (where applicable). Existing entries without `wraps:` continue to work (no implied wrapping). - `agent-guard hook pre-tool-use` reads the config and routes from it. The hardcoded `coilyRoutes` / `agentGuardRoutes` maps are gone. - coily ships a coily.yaml with the canonical `wraps:` blocks for its ops/pkg/docker/ssh/tailscale verbs. - `agent-guard lint` enforces the two invariants above. **Followups (out of scope here)** - coily#185 (replace lockdown-deny.sh with a call to this binary) consumes the externalized table. - A consumer that ships its own cli-guard binary (e.g. `cli-mcp`) can opt into this hook by declaring its own `wraps:`. **Notes** - Pairs with agent-guard#10 (hook) and agent-guard#12 (install-hooks). - Sibling: coilysiren/coily#185.
Sign in to join this conversation.
No labels
P0
P1
P2
P3
P4
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
coilyco-flight-deck/ward#10
No description provided.