Skip to content

story-035: Grant stages the read-only tools they need, and deny mutation at the door - #33

Merged
jerodw merged 1 commit into
mainfrom
story/story-035
Aug 13, 2026
Merged

story-035: Grant stages the read-only tools they need, and deny mutation at the door#33
jerodw merged 1 commit into
mainfrom
story/story-035

Conversation

@jerodw

@jerodw jerodw commented Aug 13, 2026

Copy link
Copy Markdown
Owner

allowed_tools granted eight Bash prefixes and omitted every read-only search tool a stage actually reaches for. story-028's implementer hit six permission denials and re-did the same reading through narrower calls; each denial costs a turn and buys nothing.

What changed

Read-only breadth is granted: grep, rg, find, head, tail, wc, sort, uniq, diff, git show, git branch, git ls-files.

Mutation is denied at the door instead of by omission. hooks/bash_guard.py is a PreToolUse hook registered against Bash and passed to every stage invocation through --settings. The config comment now states the reasoning rather than leaving the list to imply it: read-only is granted broadly because the harness's own rules are enforced by the coordinator after a stage runs, not by this list, and mutation is denied by the guard rather than by these entries happening to omit it.

Why the layering is sound

Two properties of the guard are load-bearing and both are stated in its own docstring rather than left to be inferred:

It denies only. There is no allow path in the file — no input produces a decision other than deny. The allowlist remains the thing that permits; the guard is the net behind it. A command the allowlist would refuse is never admitted by the guard reporting no problem with it.

Its bias is fail-open, and that is safe precisely because of the above. Unreadable stdin, a malformed payload, an unbalanced quote, an unterminated substitution and a heredoc all yield no decision and fall through to the allowlist. A fail-open guard can only fail to add a denial; it can never grant anything. A fail-closed parser mistake would stop runs that should have proceeded, which is the more expensive error and the opposite of the one-directional bias the coordinator's other checks take.

It also states what it does not cover — a mutator spelled to avoid recognition, assembled from variables, or run through an interpreter — rather than implying completeness.

Verification

  • 1878 passed, 197 written; verification passed first iteration, no retries; clean-clone green with the story committed
  • One implementer edit under tests/ was permitted by the revert check: tests/test_harness_layer_extraction.py fails when reverted
  • That the hook fires inside a live stage invocation was not observed, since that requires a model call. What is asserted is what the criterion states: the built argument list carries --settings naming an existing absolute path

🤖 Generated with Claude Code

…ion at the door

Implemented by the l5 harness story workflow.
@jerodw
jerodw merged commit 538248f into main Aug 13, 2026
3 checks passed
@jerodw
jerodw deleted the story/story-035 branch August 13, 2026 21:58
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