feat(agentfw): daemon + Claude Code hook collector (phase 09) - #14
Merged
Merged
Conversation
Verified the real hook contract before designing, which changed three things: - permissionDecision has a fourth value, defer. Our Allow must map to defer, not allow: allow APPROVES a call into the normal permission flow, so mapping Allow onto it would auto-approve tool calls the operator's own rules would have prompted on. Installing the firewall would have weakened existing protection. - Hooks support type: http natively, which deletes the shim binary and the Unix socket. Cost is that a localhost port has no filesystem ACL, so the daemon needs its own bearer-token auth. - No hook carries a subagent's granted tools, so Authority stays dormant this phase. Subagent escalation is not covered until phase 11 — stated rather than quietly shipped as covered. Ships in shadow mode by default: every verdict computed and logged, defer always returned. The lab measured 7/15 benign follow-ups tainting; learning the real rate by having live sessions interrupted is the expensive way. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
12 TDD tasks building crates/agentfw: config, bearer-token auth, hook payload deserialization, provenance table, event mapping, verdict mapping, audit sink, daemon router, integration tests, install, replay, and CI/PR. Task 1 is a GATE, not a step: it measures what Claude Code actually does when an http hook's endpoint is unreachable. The documented exit-code semantics describe command hooks only. If an unreachable http hook blocks or hangs the agent loop, the transport decision reverts to a command shim and the rest of the plan changes. It needs an interactive session, so the implementer hands it back rather than attempting it. Code verified against the real agent crate: exports, Outcome fields, Provenance::label(), Verdict derives, AgentFirewall method signatures. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Task 4 found that serde requires session_id to be PRESENT but not non-empty. Everything -- taint, sequence numbers, session isolation -- is keyed by it, so an empty id would collapse unrelated sessions into one shared taint pool: the cross-session leak the required-field rule exists to prevent. to_event now returns None for it, which the daemon already treats as 'nothing to inspect, proceed'. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…efer) Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Measured with the hook pointed at a dead port via --settings inline JSON (nothing written to disk): the tool call proceeded and completed, at real 7.39s against a 5s hook timeout plus normal startup. The timeout is honoured and then execution continues. No block, no hang. So decision B1 stands: native http hooks, no shim binary, no Unix socket. Task 8 proceeds as designed. One cost recorded rather than glossed: a stopped daemon charges the full timeout on every tool call. Nothing breaks, but it presents as Claude Code being slow rather than as agentfw not running. install output and the README must say so, and SessionStart is a natural place to probe /health and warn. The timeout stays at 5s because phase 10's judge tier needs a 3s budget of its own. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Adds a "Running the agent firewall" section: install / serve / replay, with a sample replay summary and the interruption-rate framing that decides whether enforcement is safe to switch on. States three things plainly rather than burying them: it ships in shadow mode and why; an unreachable daemon fails open but costs the 5s hook timeout per tool call, which reads as "Claude Code feels slow" rather than as an error; and why Allow maps to defer rather than allow, since mapping it to allow would auto-approve calls the operator's own rules would have prompted about. Test badge 249 -> 336, per-crate and per-module tables extended with the daemon's 87 tests. Counts verified from the actual suite. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Phase 09 turns the phase-08 library into a running daemon wired into Claude Code's native hooks.
It ships in shadow mode. Every verdict is computed and written to the audit log; nothing is blocked until
enforce: trueis set deliberately. A lab measurement found 7 of 15 benign follow-up commands tainting — learning the real rate by having live sessions interrupted is the expensive way.Verified end to end
The same attack, run twice. Enforcing:
{"event":"pre_tool_use","rule":"deny-tainted-privilege","verdict":"deny","shadow":false, "taint":{"source":"network:blog.example.com","seq":1}, "egress_hosts":["exfil.example.com"],"tool":"Bash"}Shadow mode:
{"event":"pre_tool_use","rule":"deny-tainted-privilege","verdict":"deny","shadow":true, "taint":{"source":"network:blog.example.com","seq":1}, "egress_hosts":["exfil.example.com"],"tool":"Bash"}A poisoned page arrives via
WebFetchat seq 1; the tracker links a laterBashcommand back to it; the egress host is extracted; the rule fires. Enforcing returnsdeny. Shadow returns nothing while logging the identical would-have-been verdict. That pair is the phase.The correction this phase exists around
permissionDecisionhas four values, not three.allowapproves a call into the normal permission flow;deferleaves the operator's own rules to decide. Mapping ourAllowverdict ontoallowwould auto-approve tool calls the operator would otherwise have been prompted about — installing a security tool would weaken existing protection.Allowmaps todefer, pinned by a test asserting both the positive and the negative.A measurement that settled the architecture
The docs describe failure semantics for
commandhooks; we chosehttphooks. Measured with the endpoint pointed at a dead port: the tool call proceeds after honouring the timeout (real 7.39sagainst a 5s timeout). Fails open, does not hang — so no shim binary and no Unix socket are needed.Cost, recorded rather than glossed: a stopped daemon charges the full timeout per tool call. Nothing breaks, but it reads as "Claude Code is slow" rather than "agentfw isn't running", so
installand the README both say so explicitly.What's here
crates/agentfw— the only crate doing I/O for the agent layer. It maps, calls, records, and translates; it decides nothing. All verdict logic stays incrates/agent.agentfw serve— axum daemon, loopback-only, bearer-token authenticated with constant-time comparisonagentfw install— prints the hook block for all five events; the token goes in by env var and is never written to the settings fileagentfw replay— the interruption rate and which rules fired, which is what decides whether enforcement is safeDefects review found in already-green code
0600.session_idwas required to be present but not non-empty; an empty one would have collapsed every session into one shared taint bucket.file_pathyieldedLocalSysteminstead ofLocalProject, mislabelling the audit log that phases 10 and 12 both depend on.truncated: falsewhen content had in fact been cut.Debug, making a forensic log's on-disk schema depend on variant names. Now an explicit mapping.Known scope limits, stated rather than hidden
Authoritystays dormant until the API/MCP collectors in phase 11.updatedToolOutputexists and is deliberately unused; this phase detects and gates, it does not alter what the model reads.installprints only. It never edits your settings file.Verification
agentfw; 249 pre-existing, none broken)cargo clippy --workspace --all-targets -- -D warningscleancargo fmt --all --checkcleancargo check --all --all-featuresclean, including the candle/ML pathcore,proxy,bench, oragentpublic APIsDesign:
docs/superpowers/specs/2026-07-29-agent-firewall-09-daemon-hooks-design.mdPlan:
docs/superpowers/plans/2026-07-29-agent-firewall-09-daemon-hooks.md🤖 Generated with Claude Code