A Claude Code plugin that keeps secrets, stray documents and destructive commands out of agent sessions. Every blocked action comes with a safe alternative.
It is one hook, written in Python with no dependencies, that runs before every Read, Edit, Write, Grep and Bash call, and on every message you send. It reads the first 64 KB of a file the tool is about to touch, which is the whole file in almost every case. The file content never reaches the model. Only the decision does. A message that holds a key is stopped before it is sent, and Claude Code erases it.
| Rule | Default | What it does |
|---|---|---|
secrets.files |
deny | Refuse to read, edit, grep or shell-touch a file whose content looks like a key or credential |
secrets.write |
deny | Refuse to write a private key block into any file |
secrets.env |
deny | When a variable name says it is a secret, refuse env, printenv and echo $NAME |
secrets.prompt |
deny | Stop a message you send that holds a key, pasted logs included, before it reaches the model. Name the line, and say to rotate it |
commit.secrets |
deny | Scan what git add and git commit are about to record. If a secret is in it, refuse |
commit.binaries |
deny | Refuse git add of PDFs, office documents, archives, databases and files over 5 MB |
commit.add_all |
deny | Refuse git add -A and git add .. Files are added by name |
commit.no_verify |
deny | Refuse --no-verify on commit and push |
commit.force_push |
ask | Confirm before a force push |
commands.fatal |
deny | Refuse rm -rf on /, ~ or ., mkfs, dd onto a disk, fork bombs |
commands.destructive |
ask | Confirm before git reset --hard, git clean -f, docker prune, DROP TABLE, terraform destroy, kubectl delete, migration resets, cloud CLI deletes, docker compose down -v and similar |
commands.sudo |
ask | Confirm before any command run as root |
exfil.pipe_to_shell |
deny | Refuse a download piped into an interpreter that would run it, such as curl ... | sh. A pipe into python3 -c or jq is data and passes |
exfil.upload |
ask | Confirm before curl uploads, scp, rsync, sftp or nc to a host that is not on the allowlist |
self.protect |
deny | Refuse edits to the plugin, its configuration file, Claude Code's own configuration file and the credentials file |
paths.boundary |
off | Confirm before touching a file outside the working directory. Ask on the contained profile |
packages.install |
off | Confirm before pip, npm, npx, cargo, brew, pacman and apt installs. Ask on the contained profile |
session.check |
off | If .env is not ignored, or a tracked file holds a secret, warn once per session |
An ask opens Claude Code's own permission dialog, Yes or No, with the reason shown in
it. No ends the tool call, and you type what to do instead. A hook's answer is honoured in every permission
mode. With permission prompts switched off, the dialog still appears. The message check
is the one exception: there, ask lets the message through and tells Claude to say that
the key is now in the transcript and must be rotated.
The hook finds secrets by content. Claude Code's own configuration can also refuse files
by name, before any hook runs. cguard carries 518 such rules for Linux, macOS and Windows
and installs them into ~/.claude/settings.json on request, adding only what is missing.
They cover SSH keys, environment files, cloud and password-manager credentials, developer
tool tokens, shell history, browser profiles and keychains. They also cover the commands
whose job is to print a secret. The Windows paths are best effort and untested.
cguard denylist status
cguard denylist install
cguard denylist remove
Remove is the undo. It takes cguard's rules out and keeps the rest of the file. The same
row exists in the configuration screen, under Files by name, and in cguard setup. The
commands and the screens are run by you. Claude cannot edit settings.json, and cannot
run install or remove from a shell either, which is the guard's own rule. So the plugin
cannot change the rules on its own. The list is grouped in
cguard/denylist.py with one comment per group, so the source is the documentation.
Two profiles. standard guards secrets, commits and the machine, and lets Claude move
between folders and install without asking. contained adds containment: Claude stays
inside the project, asks before touching anything outside it or installing anything, and
the session check runs. Every rule can be set to deny, ask or off on top of either profile.
A guard never says only "no". Every refusal Claude receives has the same shape: what was blocked, why in one line, how to do it yourself, and how to allow it. For example:
cguard deny [commit.binaries]: `git add` of /home/me/app/docs/case.pdf: case.pdf is a .pdf file.
Why: documents and large files in a repository are usually accidents, and history keeps them forever.
Do it yourself: move it out: `mv /home/me/app/docs/case.pdf ~/` and add the rest by name.
Or allow it: `cguard allow commit_paths docs/case.pdf`
Claude relays that to you. You are never left holding a wall.
In Claude Code, from this repository:
claude plugin marketplace add simon-init/cguard
claude plugin install cguard@cguard
Each command prints a confirmation. The hook takes effect in Claude Code sessions started after the install, so restart any session that is already open.
Then put the command line on your path, once. On Linux and macOS:
mkdir -p ~/.local/bin
ln -sf ~/.claude/plugins/marketplaces/cguard/bin/cguard ~/.local/bin/cguard
Both commands print nothing when they work. ~/.local/bin is not there on every machine,
so mkdir -p makes it, and leaves it as it is when it already exists. Check the result:
cguard --help
It should print a list of commands that starts with cguard: guards for a Claude Code session. If it says command not found: cguard instead, ~/.local/bin is not on your
PATH yet. Add it, once:
echo 'export PATH="$HOME/.local/bin:$PATH"' >> ~/.zshrc # bash: ~/.bashrc
This also prints nothing, and it does not change the terminal you are in: it adds a line
to the file your shell reads when it starts. Open a new terminal, or run
source ~/.zshrc (bash: source ~/.bashrc), and cguard --help works. Run the echo
line only once, since every run adds the line again.
On Windows, add this folder to your PATH. The launcher in it is cguard.cmd:
%USERPROFILE%\.claude\plugins\marketplaces\cguard\bin
The hook needs Python 3.9 or later and git. python3 --version shows yours; 3.10, 3.11,
3.12, 3.13 and newer all count as later than 3.9. Nothing else is installed. The two screens,
cguard config and cguard setup, need the curses module. Linux and macOS have it.
Windows Python gets it with pip install windows-curses, and every other command works
without it. On Windows, Claude Code runs hooks in Git Bash when Git for Windows is
installed, and the hook needs that. The Windows side follows the documentation and is not
yet tested on a Windows machine. A report is welcome.
Once after install, in a terminal:
cguard setup
Run it in a terminal window, not through Claude Code: inside a session it only prints
cguard setup is a screen and needs a terminal. In a terminal, it opens a screen with five steps: the profile, the known secret paths, the computers you trust,
the commands worth knowing, and a summary. Up and down move between the answers, and
Enter picks one. Every step waits for you, and the left arrow goes back. Nothing is
written until the last step, except the known secret paths, which are written when you
choose to add them. It is safe to run again. Until the configuration file exists, a
session that starts with the plugin installed gets one line asking Claude to point you
at setup.
The hook knows two sets of formats. The first set is in cguard/patterns.py, written
for this plugin. The second set is imported from the gitleaks rule set, MIT, and lives
in the generated file cguard/rules_gitleaks.py. The import keeps only rules with a
fixed prefix. Rules that match a keyword plus any random string are left out on purpose.
In a hook that blocks work, such rules refuse real files. The import script is
tools/sync_gitleaks.py. The license notice is in THIRD_PARTY_LICENSES.md.
From any terminal, or by asking Claude to run it:
cguard show what is on, and the allowlists
cguard set commit.add_all ask change one rule
cguard allow hosts my-server.example.com
cguard allow paths ~/other-project
cguard allow commit_paths fixtures/sample.pdf
cguard profile contained switch profile
cguard audit the last decisions
cguard why the last refusal, explained
cguard why list the last twenty refusals, numbered; cguard why 3 explains one
cguard check 'rm -rf build' dry-run a command
The interactive screen, for a person in a terminal:
cguard config
The keys on that screen:
- Up and down move between rules.
- Enter or space cycles a rule's mode.
dsets deny,asets ask,osets off. - The right arrow opens the full description, with how the rule works and what it does not catch. Up and down scroll it.
- The left arrow returns to the list.
pswitches profile.ssaves andqquits.
The first entry, About cguard, explains the mechanism, the detectors and their limits.
Inside a Claude session, /cguard:config, /cguard:audit and
/cguard:why show the same things.
The configuration lives in ~/.claude/cguard.json. The hook reads the file on
every call, so a change applies to the next tool use without a restart.
Every refusal and every ask is appended to ~/.claude/cguard.log as one JSON line:
time, tool, rule, mode, what, and the target with any secret masked. It stays on the
machine. To log every shell command as well, turn on audit.commands in the
configuration file.
- It runs one Python script,
cguard/hook.py, before each file and shell tool call, and once at session start. Nothing is installed and nothing runs in the background. - It stores two files under
~/.claude:cguard.json, the configuration, andcguard.log, the audit log. On your command,cguard denylist installadds deny rules to~/.claude/settings.json, and nothing else touches that file. - It sends nothing anywhere. The hook makes no network calls. The one script that
reaches the internet is
tools/sync_gitleaks.py. A developer runs it by hand to refresh the imported key formats from GitHub, and it is not part of the hook.
Say this plainly, because a guard that is trusted beyond what it does is worse than none.
- It reads the first 64 KB of a file, and no more than 4 MB per tool call. A secret further down in a large file passes.
- It reads shell commands as text. A path built by a subshell, an encoded argument, or a file read by a program that Claude wrote and then ran, is not seen.
- It knows the formats of common keys and the shape of
KEY=valuepairs. A secret that looks like ordinary text passes. So does a bare random string with no name in front of it and no known prefix, in a file or in a message. - It is a hook inside Claude Code. It does not sandbox anything. A model that is
determined to misbehave has other routes. The deny list in
settings.jsonand a real sandbox are the layers below this one.
It is a guardrail for the ordinary case: the accident, the careless command, the file in the wrong folder. Those are most of the incidents that happen.
python3 -m unittest discover -s tests -v
The unit tests cover the guards without a model, the commit gate included. The evals/
folder holds two behaviour cases for Claude Code's plugin evals: a project with a secret
in .env, where the key must not reach the reply, and an ordinary configuration file,
which must be read without a refusal. Each case seeds its workspace with a script, so the
run needs the scaffold flag. The runs call the model on your account.
claude plugin eval . --scaffold
There is no eval case for the commit gate. The eval sandbox masks the git binary as a credential path, so no run can make a commit, with or without the plugin.
The hook runs on your machine and sends nothing anywhere. PRIVACY.md says what it reads, what it keeps and what it sends, in one page.
MIT.