story-035: Grant stages the read-only tools they need, and deny mutation at the door - #33
Merged
Conversation
…ion at the door Implemented by the l5 harness story workflow.
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.
allowed_toolsgranted 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.pyis aPreToolUsehook 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 committedtests/was permitted by the revert check:tests/test_harness_layer_extraction.pyfails when reverted--settingsnaming an existing absolute path🤖 Generated with Claude Code