deny-probe
A penetration tester for Claude Code permission deny rules. You write
Read(./.env) thinking your secrets are safe — deny-probe shows you the
11 other roads into the same file.
The problem
Claude Code enforces permissions.deny rules per tool. A rule like
Read(./.env) blocks the Read tool, but says nothing about:
- the Grep tool (
Grepwith pattern.dumps the whole file), - Bash (
cat,grep -r,sed,awk,python3 -c "print(open(...).read())"…), - CLAUDE.md
@importchains (@./.envin CLAUDE.md pulls the denied file into context through aRead(./CLAUDE.md)call the rule doesn't cover), - nested
CLAUDE.mdfiles that are auto-loaded.
deny-probe ships a built-in library of 18 such bypass routes, predicts statically which of your deny rules stop which route, and tells you exactly which rules to add.
Install
pip install deny-probe
Zero dependencies — stdlib only. Requires Python 3.9+.
Usage
Static audit (the main event)
# Audit the deny rules from your Claude Code settings.json
deny-probe audit --settings ~/.claude/settings.json
# ...or pass rules directly
deny-probe audit --deny 'Read(./.env)' --deny 'Read(./secrets/**)'
# Protect a different file, machine-readable output
deny-probe audit --settings settings.json --target ./config/keys.json --format json
Example output for a Read(./.env)-only config:
Target: ./.env
Deny rules (1):
Read(./.env)
ROUTE TOOL VERDICT BLOCKED BY
----------------------------------------------------------------------
read-direct Read HOLD Read(./.env)
grep-tool Grep LEAK -
glob-tool Glob META -
bash-cat Bash LEAK -
bash-grep Bash LEAK -
...
bash-python Bash LEAK -
claude-md-import Read LEAK -
claude-md-nested Read LEAK -
Summary for ./.env: 16/18 routes leak content, 1 hold, 1 metadata-only.
Hardening suggestions:
[grep-tool] Grep tool search
+ Grep(./.env)
[bash-cat] Bash: cat
+ Bash(cat *)
[bash-python] Bash: python one-liner
+ Bash(python *)
+ Bash(python3 *)
...
Verdicts: HOLD (a deny rule blocks the route), LEAK (file content reaches the model), META (only metadata leaks, e.g. Glob reveals the file exists but not its content).
Exit code is 1 when any route leaks content, 0 otherwise — so you can
gate on it in CI:
- run: deny-probe audit --settings .claude/settings.json --target ./.env
Live verification (manual only)
--live replays a route against real claude -p sessions using a canary
marker, and checks whether the marker appears in the answer (and optionally
in a transcript file):
deny-probe live --target ./.env --route bash-cat --runs 3
deny-probe live --target ./.env --all --runs 5 --format json
Live mode needs the claude CLI on PATH, is nondeterministic, and never
runs in CI.
Route library
deny-probe routes
Lists the 18 built-in bypass routes with descriptions and preconditions. Head and tail are separate routes so both command variants are evaluated.
Five of the routes are regression tests for real Claude Code
permission-matching bugs fixed in 2.1.287 / 2.1.289 (Oct 2026): reading
through a symlink path, hiding a command behind an env-var prefix
(TZ="$HOME" cat ...), a bare variable assignment (FOO=1 cat ...), a
redirection suffix (cat ... > /tmp/exfil), and a nested command inside a
compound line (echo ok && cat ...). Each models the pre-fix behavior, and
each is closed by a wildcard rule like Bash(*cat ./.env*) — exact-string
rules never match the mutated command line.
How the static prediction works
For each (target file, route): the route is HOLD iff at least one deny
rule names the route's tool and its glob pattern matches the route's
argument. * stays within one path segment for file tools, ** crosses
segments, and Bash command patterns match against the full command line.
That's the whole model — deliberately simple, so the predictions are
auditable.
Honest limitations
- The route library covers known techniques. It is not exhaustive, and new Claude Code versions can introduce new tools, new flags, or new auto-loading behaviors that open routes this library doesn't know about. Re-run after upgrades; treat a clean report as "no known bypass", not "provably safe".
- Pattern matching is an approximation. Claude Code's exact permission
matching (especially edge cases around absolute paths, symlinks, and
~) is not fully documented; deny-probe implements the documentedTool(pattern)glob behavior as faithfully as possible, but a predicted HOLD is only as accurate as that model. --liveresults are nondeterministic. The model may refuse, take a different route than the one prompted, or leak on run 3 after holding on runs 1–2. Live mode is a spot check, not a proof. Run it several times before drawing conclusions.- Command-prefix blocklists are fragile. Suggestions like
Bash(python *)block the exact prefix; renamed binaries, new interpreters, and creative flags can slip past. For real secrets, prefer keeping them out of the agent's working directory (environment-backed secret stores,ask-gated hooks) over longer deny lists. - Preconditions matter. The CLAUDE.md routes assume the
@import(or the nested file) actually exists. deny-probe reports what would happen given your rules, not what your repo contains.
Development
python -m venv .venv && .venv/bin/pip install pytest
.venv/bin/python -m pytest tests/ -q
55 tests, all static-fixture assertions. --live is excluded from CI by
design.
License
MIT — see LICENSE.
Metadata
Release files for deny-probe 0.3.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| deny_probe-0.3.0.tar.gz | 24.5 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| deny_probe-0.3.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 42.9 kB
Release files / deny_probe-0.3.0.tar.gz
| Download URL | deny_probe-0.3.0.tar.gz |
|---|---|
| Size | 24.5 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
a64b59f8628ffea3f0ee13b4fae701902b9b3a559afa1130376d55dcf19f7091
|
|
BLAKE2b-256 checksum How to use checksums |
9231db34597a5808f171ce4da59348f38ca32d89d2e2c637351700a133b18976
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.12.3
|
Release files / deny_probe-0.3.0-py3-none-any.whl
| Download URL | deny_probe-0.3.0-py3-none-any.whl |
|---|---|
| Size | 18.4 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
9153b40bce000c1652505b639ab4a1fcd592c87f241c8126f8ddb908abb97f0e
|
|
BLAKE2b-256 checksum How to use checksums |
b08a1074a33092bd540b2b232e37e3a061b9cfcd2866ede31292b5a67fe8831c
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.12.3
|