Block all unix file reads when code-context is installed - #40
Open
ekechinwokah wants to merge 4 commits into
Open
Block all unix file reads when code-context is installed#40ekechinwokah wants to merge 4 commits into
ekechinwokah wants to merge 4 commits into
Conversation
An agent that greps a codebase reasons from match fragments, and fragments produce confident wrong claims about code it never read. The plugin now ships a guard: a PreToolUse hook (cx guard --hook) that denies the Grep tool and grep/egrep/fgrep/rg/git-grep at command position in Bash - through pipes, env prefixes, wrappers (env, xargs, command, timeout...), paths, and alias escapes - while leaving argument uses like 'git log --grep=' alone. bm25_search covers the pinpoint exact-identifier case with chunks that carry their content. The decision is a pure function (src/core/guard.ts) with the tokenizer deliberately coarse: unmodeled quoting can only inspect MORE segments, so quoting cannot smuggle a grep past it. The hook never fails the session: unparseable events allow silently, matching cx usage --hook. Docs updated to match: the 'grep wins pinpoint lookups' guidance is replaced by the block-and-why. The hooks.json pin (@0.4.0) tracks .mcp.json and needs the same release-prep bump.
With grep blocked by the guard, log spelunking has to go through the index too, and the allowlist excluded exactly those files. They chunk as fixed windows (no grammar), which is what a log wants; the 1MB CX_MAX_FILE_BYTES cap still applies, so a giant trace is skipped and flagged partial rather than swallowing the index.
Blocking grep alone moves the behavior rather than stopping it. With grep denied, an agent reaches for the next line-filter within reach — awk, sed — prints three matching lines, and answers from those instead of reading the file or asking the index. The failure being blocked is reasoning from an extracted fragment, not the name of the program that extracted it, so the stream editors that make it easy are blocked too. Editing a file in place belongs to the Edit tool and reading one belongs to Read or to the ranked index; neither use of awk or sed is lost. Filtering flags such as the one on git log are unaffected, since matching is still on the executed program at command position. Adds the same cases to the local standalone hook, plus a small CommonJS checker for it: the vitest suite needs a newer Node than some dev boxes run, and the guard is worth being able to verify anywhere.
Blocking one program at a time does not hold. With the grep family denied, the next reach is awk, then sed, then cut, then a one-line python, and the agent still answers from the handful of lines whichever one printed. The failure is reasoning from an extracted fragment, not the name of the program that extracted it, so the line to hold is that no file contents arrive through a shell at all. Denies the whole family together: the readers and pagers (cat, head, tail, tac, nl, less, strings, od, xxd), the text filters (cut, tr, sort, uniq, paste, join, comm, column, jq, yq), and interpreters run as one-liners, where python3 -c, node -e, perl -pe and a heredoc piped to stdin are a text filter under another name. Running a script from a file stays allowed, as does everything that does work rather than reads: builds, tests, benchmarks, version control, file moves, redirects into files. Only "show me the bytes" moves to Read and to the index, and the denial message now says so. Neither is a downgrade, since Read takes an offset and the index answers over a corpus of any size, so size is never a reason to filter. The local standalone hook carries the same rules, checked by 47 cases: 30 denied across pipes, wrappers, env prefixes, absolute paths and alias escapes, and 17 allowed covering the ordinary working commands.
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.
Claude consistently tries to get around code context. Force it. The quality/speed is much better and it hallucinates less.
What's blocked now — everything that puts file bytes in front of me through a shell:
grep family: grep, egrep, fgrep, rg, ag, ack
stream editors: awk, gawk, mawk, nawk, sed, ed
readers and pagers: cat, tac, head, tail, nl, less, more, strings, od, xxd, hexdump
text filters: cut, tr, sort, uniq, paste, join, comm, column, fold, rev, expand, unexpand, jq, yq
interpreters run inline: python3 -c, python3 -, heredoc-to-stdin, node -e, perl -pe, ruby -e
Caught through pipes, wrappers (xargs, env, timeout), env prefixes, absolute paths, and \ alias escapes — so cargo test | tail -20 and TMPDIR=/x awk … both die.
What still works: cargo, make, git (including --grep= flags and commit -F), bash script.sh, python3 script.py, node build.js, ls, cp, mv, mkdir, pgrep, and redirects into files. Building, benching, committing — untouched.
47 cases green (30 denied, 17 allowed), mirrored into the branch's vitest suite. The denial message now points at Read with its offset argument and at the index, and says outright that size is never a reason to filter.