feat(guard): a policy implementation may be Python, not only shell - #137
Merged
Merged
Conversation
Three coupled limits made a .py guard invisible end to end. Discovery in
_guard_script matched <policy_id>.sh only, so a Python implementation was never
found; run_guard hardcoded [bash, guard, *argv], so one handed to bash would fail;
and Policy.guards globbed implementations/*.sh, so its eval suite silently demoted
to tier 3 with no executable form. Each is a separate file and each hid the next.
Discovery now walks GUARD_SUFFIXES, shell first, so a policy shipping both is
deterministic rather than filesystem-ordered. run_guard picks the interpreter from
the suffix -- this Python for .py, the existing bash probe otherwise -- and the
"no usable bash" message becomes "no usable interpreter", since the condition it
reports is no longer bash-specific.
Plugin packaging needed no change: all four packagers already call _guard_script,
so a Python guard now reaches plugin.json and the hooks file by the same path a
shell one does. That was the point of fixing discovery rather than special-casing
the emitters.
What this does NOT change: a guard still receives shlex.split(command), so it
judges a shell command. Guards that need file content -- the write-tool surface the
adapters already extract `content` and `path` for, and which vendors.shell_matcher
cannot emit a matcher for -- remain unavailable. That is a separate change to the
input contract, not a follow-up to this one.
Verification:
pytest -q -> 1136 passed, 6 skipped
pytest acceptance/ -> 21 passed
ruff check . && ruff format --check . -> clean, 262 files
chock check --only {matrix,mechanisms,validate,index,conflicts,evals,verify}
chock check --only validate --mode {frontier-claude,frontier-devin}
chock sync --repo . --check -> all PASS
tools/check_literal_duplication.py -> no repeated literals
Break-and-restore, each change reverted alone to prove a named test pins it:
discovery -> test_discovery_finds_a_python_guard, test_the_plugin_packages_the_python_guard
interpreter -> test_a_python_guard_blocks_and_allows
eval glob -> test_a_python_guard_makes_the_suite_deterministic
The vendored runtime is embedded in all ten adapters, so .chock/bin and the frozen
runtime goldens move with guard_runner. Regenerated with CHOCK_REGEN_GOLDENS=1 and
chock sync, not by hand; every adopter's next sync rewrites .chock/bin.
Signed-off-by: Claude <noreply@anthropic.com>
jothimani-rajendran
marked this pull request as ready for review
September 10, 2026 10:54
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.
What
A policy could only ship a guard as
<policy_id>.sh. Three coupled limits made a Python implementation invisible end to end, each in a different file, each hiding the next:.pyguardin_agent._guard_script<id>.shonlyguard_runner.run_guard[bash, guard, *argv]hardcodedeval.suites.Policy.guardsimplementations/*.shDiscovery now walks
GUARD_SUFFIXES— shell first, so a policy shipping both is deterministic rather than filesystem-ordered.run_guardpicks the interpreter from the suffix: this Python for.py, the existing bash probe otherwise. The"no usable bash"message becomes"no usable interpreter", because the condition it reports is no longer bash-specific.Plugin packaging needed no change. All four packagers (
claude,codex,copilot,cursor) already call_guard_script, so a Python guard now reachesplugin.json, the hooks file and the packaged scripts by exactly the path a shell one does. That was the point of fixing discovery rather than special-casing four emitters.What this deliberately does not change
A guard still receives
shlex.split(command)— it judges a shell command. Guards that need file content stay unavailable, and that is a separate change to the input contract, not a follow-up to this one:vendors.shell_matcheris the only matcher helper in the compiler, so every emittedPreToolUsehook matches shell tools.content(tool_input.new_string,tool_input.edits[].new_string, …) andpathforWrite/Edit/MultiEdit/NotebookEdit, with evidence basislive-run— but nothing can emit a hook that would ever deliver those payloads.verify-mcp-allowliststates the consequence in its own manifest: "a file-write tool bypasses this guard."So this PR makes Python guards first-class; it does not add a write-tool surface. No coverage claim moves: a Python guard installs the same surface at the same tier as a shell one,
INSTALLED_SURFACESis language-agnostic, and--only matrix/--only mechanismsboth pass.Definition of done
chock check→ 0 errors, 0 warnings, 0 infoschock check --only matrixpasses; no matrix change needed — behaviour tier is unchangedchock sync --repo . --checkcleanchock check --only verifycleanpytest -qgreen — 1136 passed, 6 skipped; new capability has both block and allow testspytest acceptance/ …green — 21 passed (packaging and hook installation are touched).shpolicy is unaffected, and discovery still prefers.shruff check .andruff format --check .clean (262 files)Also run, enumerated from
ci.ymlrather than from this checklist:--only mechanisms,--only index,--only conflicts,--only evals,--only validate --mode frontier-claude,--only validate --mode frontier-devin, andtools/check_literal_duplication.py(no repeated literals —".py"appears in two modules becauseguard_runneris vendored standalone into adopters and cannot import fromchock.compile).Claims
docs/orINSTALLED_SURFACESties a guard's tier to its script language, so no published claim moves.Break-and-restore
Each change was reverted on its own to prove a named test pins it, rather than reverting all three and watching the import fail:
The generated-artifact ripple, stated because it reaches every adopter
guard_runneris embedded in all ten vendored adapters, so.chock/bin/*.pyand the frozen runtime goldens move with it.test_runtime_goldensfailed exactly as designed — "every adopter's next sync rewrites.chock/bin" — and was regenerated deliberately withCHOCK_REGEN_GOLDENS=1, then.chock/re-synced withchock sync --repo .. Nothing hand-edited.That is the one thing worth a reviewer's attention: this PR changes bytes in every adopter's vendored runtime. The functional delta is small and additive (a new
find_interpreter, one substituted variable, one message), but it will show up as a rewrite on the next sync in every repo that has chock installed.🤖 Generated with Claude Code
Generated by Claude Code