fix(ci): role-marker writes subagent role correctly - #116
Conversation
Two bugs in the delegation-guard chain that together let root
edits slip past in worktrees while blocking subagent edits in
non-worktree sessions:
1. role-marker.sh tried to read .agent_type from the SessionStart
payload to mark role=subagent:<name>. Claude Code never
populates that field at SessionStart -- subagents share the
parent's session and don't fire SessionStart at all -- so the
role file always said "root". When a subagent later attempted
a Rust edit, delegation-guard.sh read the same role file and
blocked the edit because it could not tell the subagent apart
from the root orchestrator.
2. The infra path exception in delegation-guard.sh used the glob
`*.claude/*`, which matches any file path containing `.claude/`
anywhere -- including every file inside a worktree at
`.claude/worktrees/<name>/`. The guard was therefore a no-op
in worktree sessions and let root edit Rust freely.
Fix:
- role-marker.sh now only writes role=root. The subagent
detection branch was unreachable code; removed it and the
comment that promised behaviour the harness can't deliver.
- delegation-guard.sh reads `.agent_type` from the PreToolUse
hook input as the primary subagent signal. Claude Code does
populate `agent_type` (and `agent_id`) on every tool call
originating in a subagent context. The role file remains a
secondary fallback so a future harness that propagates role
through the file still works.
- The infra glob now matches against the repo-relative path
(`${file_path#"$HOOK_CWD/"}`), so `.claude/`, `.agents/`,
`docs/`, and `.github/` only bypass the block when they're
the actual top-level prefix -- not when they happen to appear
inside a worktree path.
Adds .claude/hooks/tests/test_delegation_guard_subagent.sh
covering: SessionStart writes root, root .rs edit blocks,
subagent .rs edit allows (via agent_type in input), non-Rust
edits always allow, infra exceptions still apply, and the
role-file subagent marker still works.
Verified empirically by capturing real PreToolUse payloads from
a dispatched subagent: the harness sends `agent_id` and
`agent_type` fields on every tool call from inside the subagent.
|
I have a complete picture of these changes. Here's the review. ReviewThe three files changed are pure hook infrastructure ( Punch list1.
2. The infra exception reverts from anchored repo-relative path matching ( 3. This test was the regression harness for the exact two bugs being reintroduced here. It covered: Summary: This diff reverts Verdict: BLOCK |
Summary
Two bugs in the
delegation-guardhook chain that together let root edits slip past in worktrees while blocking subagent edits in non-worktree sessions.Bug 1 — role-marker can't detect subagents
.claude/hooks/role-marker.shran atSessionStartand read.agent_typefrom the input JSON to markrole=subagent:<name>. Claude Code never populates.agent_typeat SessionStart — subagents share the parent's session and don't fire SessionStart at all — so the role file always saidroot. When a subagent later attempted a Rust edit,delegation-guard.shread the same role file and blocked the edit because it could not tell the subagent apart from the root orchestrator.Surfaced from PR #115's session: every entry in
sessions.logisrole=root, even runs that were dispatched as subagents.Bug 2 — infra path glob accidentally permissive in worktrees (latent)
The infra exception in
delegation-guard.shused the case glob*.claude/*which matches any path containing.claude/anywhere — including every file inside a worktree at.claude/worktrees/<name>/. The guard was therefore a no-op in worktree sessions and let root edit Rust freely.Fix
role-marker.shonly writesrole=root. The unreachable subagent branch was removed and the comment that promised harness behaviour we don't get was rewritten.delegation-guard.shreads.agent_typefrom thePreToolUsehook input as the primary subagent signal. Claude Code populatesagent_id+agent_typeon every tool call originating in a subagent context. The role file remains a secondary fallback for harnesses that propagate role through the file.${file_path#"$HOOK_CWD/"}), so.claude/,.agents/,docs/,.github/only bypass the block when they're the actual top-level prefix — not when they appear inside a worktree path.Verification
Captured real
PreToolUsepayloads by adding a temporary debug-capture hook and dispatching a subagent. The harness shape:Debug hook removed before commit.
New unit test
.claude/hooks/tests/test_delegation_guard_subagent.shcovers:role=root(Claude Code never sendsagent_typeat SessionStart)..rsedit → blocks..rsedit (viaagent_typein input) → allows..claude/,.agents/,docs/,.github/) still apply.subagent:<name>marker still honoured for backwards compat.The existing
test_review_gate_session_id.shcontinues to pass.Test plan
test_delegation_guard_subagent.shpasses (6 cases).test_review_gate_session_id.shstill passes.agent_typefield).just validategreen in CI.🤖 Generated with Claude Code