Skip to content

feat(agentfw): daemon + Claude Code hook collector (phase 09) - #14

Merged
carbon-evolution merged 18 commits into
mainfrom
feat/agent-firewall-09
Jul 30, 2026
Merged

carbon-evolution merged 18 commits into
mainfrom
feat/agent-firewall-09

Conversation

@carbon-evolution

Copy link
Copy Markdown
Owner

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: true is 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 WebFetch at seq 1; the tracker links a later Bash command back to it; the egress host is extracted; the rule fires. Enforcing returns deny. Shadow returns nothing while logging the identical would-have-been verdict. That pair is the phase.

The correction this phase exists around

permissionDecision has four values, not three. allow approves a call into the normal permission flow; defer leaves the operator's own rules to decide. Mapping our Allow verdict onto allow would auto-approve tool calls the operator would otherwise have been prompted about — installing a security tool would weaken existing protection. Allow maps to defer, pinned by a test asserting both the positive and the negative.

A measurement that settled the architecture

The docs describe failure semantics for command hooks; we chose http hooks. Measured with the endpoint pointed at a dead port: the tool call proceeds after honouring the timeout (real 7.39s against 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 install and 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 in crates/agent.

  • agentfw serve — axum daemon, loopback-only, bearer-token authenticated with constant-time comparison
  • agentfw install — prints the hook block for all five events; the token goes in by env var and is never written to the settings file
  • agentfw replay — the interruption rate and which rules fired, which is what decides whether enforcement is safe

Defects review found in already-green code

  • Token file was written then chmod'd, leaving a window at the process umask. Now created 0600.
  • session_id was required to be present but not non-empty; an empty one would have collapsed every session into one shared taint bucket.
  • A relative file_path yielded LocalSystem instead of LocalProject, mislabelling the audit log that phases 10 and 12 both depend on.
  • The audit log claimed truncated: false when content had in fact been cut.
  • The audit event name was derived from Rust's Debug, making a forensic log's on-disk schema depend on variant names. Now an explicit mapping.

Known scope limits, stated rather than hidden

  • Subagent authority containment is not covered. No hook exposes a subagent's granted tools, so Authority stays dormant until the API/MCP collectors in phase 11.
  • No result rewriting. updatedToolOutput exists and is deliberately unused; this phase detects and gates, it does not alter what the model reads.
  • install prints only. It never edits your settings file.

Verification

  • 336 workspace tests passing, 0 failing (87 new in agentfw; 249 pre-existing, none broken)
  • cargo clippy --workspace --all-targets -- -D warnings clean
  • cargo fmt --all --check clean
  • cargo check --all --all-features clean, including the candle/ML path
  • No changes to core, proxy, bench, or agent public APIs

Design: docs/superpowers/specs/2026-07-29-agent-firewall-09-daemon-hooks-design.md
Plan: docs/superpowers/plans/2026-07-29-agent-firewall-09-daemon-hooks.md

🤖 Generated with Claude Code

carbon-evolution and others added 18 commits July 29, 2026 22:28
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>
@carbon-evolution
carbon-evolution merged commit 6d75327 into main Jul 30, 2026
2 checks passed
@carbon-evolution
carbon-evolution deleted the feat/agent-firewall-09 branch July 30, 2026 02:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant