Skip to main content

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 (Grep with pattern . dumps the whole file),
  • Bash (cat, grep -r, sed, awk, python3 -c "print(open(...).read())" …),
  • CLAUDE.md @import chains (@./.env in CLAUDE.md pulls the denied file into context through a Read(./CLAUDE.md) call the rule doesn't cover),
  • nested CLAUDE.md files 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 documented Tool(pattern) glob behavior as faithfully as possible, but a predicted HOLD is only as accurate as that model.
  • --live results 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)

Source distribution for deny-probe 0.3.0
File Size Uploaded
deny_probe-0.3.0.tar.gz 24.5 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for deny-probe 0.3.0
File Interpreter ABI Platform
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

Release history Release notifications | RSS feed

0.4.0

2 release files

This release

0.3.0 This release

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page