Judge an edit as the file it would leave, not as its fragment - #168
Merged
Merged
Conversation
Claude Code changes an existing file with Edit or MultiEdit, whose payload carries only the text replaced and the text put in. The write gate judged that fragment alone -- no imports, no class, no neighbours -- so a policy that reads whole files found nothing and the edit went through; only the Stop hook refused it afterwards. The first real Claude Code run of the java-security agent kit showed exactly that. An edit is now judged as the file on disk with the call's replacements applied in order (replace_all honoured, CRLF files matched), for Claude Code's spelling and the oldString/newString and old_str/new_str ones. A kind reading added lines still judges only the introduced text: the payload carries it as `added`, which an older runner ignores. When the file cannot be rebuilt the fragment is judged as before. The edit helpers live in gate/edit_image.py, bundled beside write_gate. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Claude <noreply@anthropic.com>
jothimani-rajendran
marked this pull request as ready for review
September 26, 2026 21:40
11 tasks
Codex CLI writes through apply_patch, whose only argument is the patch in tool_input.command -- no path, no content -- so the write gate had nothing to judge. gate/patch_image.py (stdlib, bundled beside edit_image) parses Codex's documented patch format and rebuilds each added or updated file from disk: hunks in order, @@ anchors, *** End of File, *** Move to, trailing-space tolerance. A hunk that cannot be placed leaves that file to the turn's end. added_lines sees only the + lines. Through the vendored codex runtime, a patch adding concatenated SQL is now denied by the real java-security gate (0.11.3: allowed). Codex's hooks still call the gate only at Stop; wiring PreToolUse for its write tool is a separate change. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Claude <noreply@anthropic.com>
9 tasks
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
The first real Claude Code run of the java-security agent kit (Windows) showed Claude change
OrderRepository.javawith Edit, adding"... WHERE customer = '" + customer + "'". PreToolUse allowed it; only the Stop hook refused it afterwards.Cause: an Edit/MultiEdit payload carries only the text replaced and the text put in, and the write gate judged that fragment alone. It has no imports, class or neighbours, so a whole-file policy (java-security's script gate) found nothing in it. Claude's exact edit gets no finding as a fragment, and
persistence-sql-string-concatat line 38 as the file it leaves.Fix, part 1: edits (
gate/edit_image.py, stdlib, bundled into every runtime).replace_all, MultiEditedits[], CRLF files, and theold_string/new_string,oldString/newStringandold_str/new_strspellings.Fix, part 2: Codex
apply_patch(gate/patch_image.py, stdlib, bundled).apply_patch, whose only argument is the patch text intool_input.command.@@anchors;*** End of Fileand*** Move to;Unchanged behaviour:
added_linesstill means only the introduced text. The payload carries it asadded, which an older runner ignores.Proof against the real policy, using Claude's actual Edit payload in an agent-kit workspace:
.chock/bin/claude_code.py(repo route)Codex's
apply_patchpayload through.chock/bin/codex_cli.pyagainst the real gate: deny on this branch, allow on 0.11.3.Definition of done
chock check→ 0 errors, 0 warnings, 0 infoschock check --only matrixpasses; no surface or claim changeschock sync --repo . --checkclean (vendored runtimes regenerated)chock check --only verifycleanpytest: newtests/test_write_gate_edits.py(24) andtests/test_write_gate_patch.py(29).edit_imageandpatch_imageare at 100% line and branch coverage.main.staged_blobgate sees the whole file, while anadded_linesgate sees only the edit or patch additions.python3lacksjsonschema:test_validate_hook_interpreter. It passes in CI.pytest acceptance/ …: 21 passed.chock/bin/*regeneratedUnreleasedentries addedruff check .andruff format --check .cleanClaims
Found while doing this (not fixed here): Gemini CLI's pre-tool write fragment (
gemini_cli-write-hooks.json) is emitted but never installed.in_agent_genericmerges only<vendor>-hooks.json, so.gemini/settings.jsongets the Stop hook alone.🤖 Generated with Claude Code
https://claude.ai/code/session_01CzNYfzP8ymU3r4JB9Sz8Ha