Guard hooks
Block secrets and PII from leaving your machine through a coding agent, using xgrep as a Claude Code or OpenAI Codex hook.
Guard hooks
xgrep guard runs as a hook inside Claude Code or OpenAI Codex and checks what the agent
is about to do before it happens:
- Secrets and PII in your prompt text and in tool arguments — an API key, an SSN, a credit card — before they are sent off your machine. Detection uses xgrep's built-in secret and PII rules with their offline validators, so placeholder/example values don't trigger false alarms.
- Dangerous commands a tool is about to run, on Unix and Windows — piping remote
content into a shell (
curl … | sh), a reverse shell,rm -rf, exfiltrating local secrets, or the PowerShell equivalents (a download-cradle /Invoke-Expression, and obfuscated-EncodedCommandpayloads, which are decoded and inspected).
When it finds something, it blocks the action (or asks, or warns) before it happens.
The guard is deterministic and local: it makes no network calls in the blocking path, and the scanned text is never written to disk.
How it works
The guard reads a hook event as JSON on stdin and returns the agent's native decision:
| Agent | Event | What it checks | On a finding |
|---|---|---|---|
| Claude Code | UserPromptSubmit | secrets/PII in the prompt you submitted | rejects the prompt |
| Claude Code | PreToolUse | secrets/PII in tool arguments and a dangerous command the tool would run | denies / asks before the tool runs |
| OpenAI Codex | PreToolUse | secrets/PII in tool arguments and a dangerous command the tool would run | denies / asks before the tool runs |
Codex has no prompt-submit hook, so on Codex the guard covers tool actions only. On Claude Code it covers both your prompts and the agent's tool calls.
Quick setup
The easiest way to configure the hook is to let xgrep write the config for you:
$ xgrep guard install --agent claude # Claude Code
$ xgrep guard install --agent codex # OpenAI CodexThis merges the hook into the agent's config idempotently — existing hooks and other settings
are preserved, and running it again is a no-op. Use --scope user to configure the agent for
all projects instead of just the current repository:
$ xgrep guard install --agent claude --scope user| Scope | Claude Code | OpenAI Codex |
|---|---|---|
project (default) | .claude/settings.json | .codex/hooks.json |
user | ~/.claude/settings.json | ~/.codex/hooks.json (or $CODEX_HOME) |
If you'd rather edit the config yourself, the manual steps are below.
Configure Claude Code manually
Add the hook to .claude/settings.json (project) or ~/.claude/settings.json (all projects):
{
"hooks": {
"UserPromptSubmit": [
{ "hooks": [{ "type": "command", "command": "xgrep guard --agent claude" }] }
],
"PreToolUse": [{ "hooks": [{ "type": "command", "command": "xgrep guard --agent claude" }] }]
}
}Configure OpenAI Codex manually
Add the hook to Codex's hooks.json:
{
"PreToolUse": [{ "command": ["xgrep", "guard", "--agent", "codex"] }]
}Options
| Flag | Default | Effect |
|---|---|---|
--agent claude|codex | (required) | Which agent's hook format to speak. |
--mode block|ask|warn | block | block denies the action; ask defers to you; warn allows it but annotates the finding. |
--fail-closed | off | On an internal error, deny instead of the default fail-open allow. |
--rules <path> | built-in | Scan with an additional rule pack (e.g. your organization's custom secret/PII patterns) instead of the built-ins. |
By default the guard fails open: if it hits an unexpected error, a malformed event, or a
timeout, it allows the action and logs the reason to .xgrep/guard.log, so a guard problem
never wedges your agent. Use --fail-closed in high-security environments where a block is
the safer failure.
Findings never include the matched secret value — the block message names the rule and the location only, so the secret is not echoed back into the transcript or the log.
Allowlisting known values
To stop the guard from blocking a sanctioned value (for example a documented example key in a
fixture), list the rule id — never the secret itself — in .xgrep/guard-allow.txt at your
repository root, one id per line (# starts a comment):
# example key used in our onboarding docs
openai-api-keyCustom organization rules
Because xgrep is Semgrep-compatible, you can add your own patterns — internal hostnames,
project code-names, forbidden APIs, production-credential shapes — as a rule pack and point
the guard at it with --rules /path/to/your/rules.
Note: with
--rules, the guard evaluates every rule in the pack, not only secret/PII rules. This is deliberate — it lets your organization block on custom patterns of any category. But it also means asecurity- or code-quality-category rule in the pack will fire and block, so keep the pack scoped to what you actually want to prevent from leaving the machine. (The built-in default, with no--rules, scans only secrets and PII.)
Scope
The guard is a prevention layer: keep secrets and PII from leaving the machine, and stop
the agent from running a genuinely dangerous command. It does not detect prompt injection or
reason about intent — those need a model in the loop and are handled separately. Its job is
the deterministic, high-precision part, and it errs toward not blocking: dangerous-command
detection uses a vetted allowlist of high-signal rules (RCE, reverse shell, rm -rf, exfil,
and their PowerShell equivalents), so an ordinary command like chmod 777 on a build
directory — or -ExecutionPolicy Bypass in a normal installer — is allowed through.
When a command runs a local script rather than an inline command — bash install.sh,
./deploy.sh, pwsh -File setup.ps1, python evil.py, node x.js — the guard also reads
that script and scans its contents, so a reverse shell (or a curl … | sh in a shell script)
hidden inside the file is caught before the tool runs it. It reads only regular files inside
your repository (never an absolute path like /etc/shadow), skips anything it cannot safely
read, and never echoes the file's contents.
For bash and PowerShell the referenced-script scan applies the full dangerous-command allowlist. Beyond the shells, the guard also scans a source file an agent is about to run or compile in any of these languages:
python, JavaScript/TypeScript, Ruby, Perl, PHP, Lua, Go, Rust, C, C++, Java, C#, Kotlin, Scala, Julia, OCaml, Swift.
It recognizes both a direct interpreter/compiler invocation (php x.php, go run x.go,
gcc x.c, javac X.java, rustc x.rs, ruby x.rb, perl x.pl) and direct execution by file
extension. For these languages the scan is scoped to three high-signal agent-safety checks and
nothing else, so it stays quiet on ordinary code:
- Reverse shell — code that opens a network socket and wires an interactive shell to it.
- Download cradle — remotely fetched content flowing into a code-execution sink
(
eval/ dynamic load / compile-and-run). - Sensitive-data exfiltration — a local secret (a secret-named environment variable, a credential or private-key file, a whole-environment dump) flowing into a network send.
The same three checks also apply when an interpreter runs the payload inline from an
argument instead of a file — php -r '…', python -c '…', ruby -e '…', perl -e '…',
node -e '…', lua -e '…', julia -e '…'. The guard pulls the inline program out of the
command and scans it as that language, so a one-liner download cradle or exfiltration is caught
just like one hidden in a script file.