AI Agents

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 -EncodedCommand payloads, 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:

AgentEventWhat it checksOn a finding
Claude CodeUserPromptSubmitsecrets/PII in the prompt you submittedrejects the prompt
Claude CodePreToolUsesecrets/PII in tool arguments and a dangerous command the tool would rundenies / asks before the tool runs
OpenAI CodexPreToolUsesecrets/PII in tool arguments and a dangerous command the tool would rundenies / 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 Codex

This 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
ScopeClaude CodeOpenAI 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

FlagDefaultEffect
--agent claude|codex(required)Which agent's hook format to speak.
--mode block|ask|warnblockblock denies the action; ask defers to you; warn allows it but annotates the finding.
--fail-closedoffOn an internal error, deny instead of the default fail-open allow.
--rules <path>built-inScan 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-key

Custom 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 a security- 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.

On this page