Status: Production-used — I run this against my own live agent workflows. · Layer: Execution controls · Portfolio map ›
Recoverability-first runtime controls for the OpenAI Codex CLI.
codex-guard sits between Codex and the tools it's about to run. Every Bash command
and apply_patch edit passes through a PreToolUse hook first, where codex-guard can
do one of four things:
- monitor the call for audit only,
- nudge Codex with additional context and let the tool run,
- deny the call with a recoverable permission decision, or
- block the call with
exit 2before it executes.
The goal isn't to sandbox Codex. It's to make agentic coding more recoverable — fail open by default, block only irreversible damage, and keep a tamper-evident audit trail of every verdict. It's the sibling of agent-guard for Claude Code; the engine ports unchanged.
Codex now executes real commands, patches files, touches secrets, mutates databases, and breaks local and dev environments. Static prompts aren't enough for that — you want runtime controls that sit between the model and the tool call.
Most agent guardrails get written before you know what the agent will actually do. codex-guard is meant to be grown from real telemetry instead: watch what Codex actually tries, find the recurring failure patterns, start them as monitor-only rules, promote the useful ones into nudges, and reserve hard blocks for irreversible operations. That's the wedge — pair it with codex-logger and you have the enforcement half of a Codex observability loop:
codex-logger shows you what Codex actually does. codex-guard turns what you learn into runtime controls.
A ~/.codex/hooks.json PreToolUse command hook routes each tool call through
pretooluse-guard.py, which matches it against your rulesets and emits one of the four
actions above. In practice that means Codex can learn from the mistakes your sessions
actually make: bare psql calls that fail, risky curl | sh installs, force-pushes to
protected branches, edits to secret files, and SQL mutations from a read-only workflow.
Absolute invariant: a guard bug must never wedge a session. Any unexpected error falls
open (exit 0).
It works because Codex fires PreToolUse — verified, not assumed
On Codex 0.140.0-alpha.2 (VS Code extension), a PreToolUse command hook was measured to:
- fire for
Bash(tool_input.command= the command) andapply_patch(tool_input.command= the patch) — not shell-only; - hard-block a call when the hook returns
exit 2(Codex shows the model "Command blocked by PreToolUse hook: …" and the tool never runs); - soft-deny a call when the hook returns JSON
{"hookSpecificOutput": {"permissionDecision": "deny", "permissionDecisionReason": …}}; - inject a non-blocking
additionalContextnudge;
and the model gracefully continued after a block. Those are the exact mechanisms this guard emits — the same contract agent-guard uses on Claude Code.
codex-logger records every Codex tool call
(with exit codes) from the rollout files. derive_rules.py reads its DB, finds commands
that fail repeatedly, and proposes log-only monitor rules for you to review before you
promote any of them:
python3 bin/derive_rules.py --db ~/.codex-logger/codex.db --days 14 --min-count 3Log real behavior → find repeated failures → derive candidate rules → review → promote the useful ones into nudges or blocks. Rules earn their way up the ladder.
Two biases, one engine:
- Main-session mode — fail-open nudges (
rules/starter.rules.json). Help Codex self-correct without interrupting flow. Rules that would fail anyway become non-blocking reminders (curl | sh, protected-branch force-pushes, inline secrets, barepsql, secret-file patches); a couple of irreversible patterns likerm -rf /hard-block. - Backstop mode — hard blocks (
rules/readonly-db.rules.json). A read-only-DB firewall that blocks detected SQL mutations (INSERT,UPDATE,DELETE,DROP,ALTER,CREATE,GRANT,REVOKE,COPY FROM,VACUUM, stored-procedure calls) withexit 2.
The philosophy: use nudges where the model can recover; use blocks where the action
violates the role. The backstop is a backstop, not a true fail-closed boundary — the hook
itself falls open on any guard error, so the durable guarantee for a read-only workflow is a
SELECT-only DB role. See docs/THREAT_MODEL.md.
python3 install.py # wire into ~/.codex/hooks.json (backed up, merge-aware)
python3 install.py --dry-run # preview the hooks.json it would write
python3 install.py --uninstall # remove just codex-guard's entryThen start a new Codex session so the hook loads. Codex uses trust-on-first-use for
hooks (it records a trusted_hash in config.toml), so it may prompt you to trust
codex-guard once. Keep pretooluse-guard.py stable and put churn in the rules JSON (which
isn't hashed) to avoid re-prompts.
A rule is JSON — a glob on tool_name, regexes on a tool_input field, an action, and a
message:
{
"id": "git-force-push-protected",
"tool": "Bash",
"any": ["\\bgit\\s+push\\b.*(--force|-f\\b).*\\b(main|master|prod)\\b"],
"action": "nudge",
"message": "Force-pushing a protected branch rewrites shared history…",
"examples": {"should_fire": ["git push -f origin main"], "should_not_fire": ["git push origin main"]}
}Actions: nudge (additionalContext, tool runs) · deny (permissionDecision deny) ·
block (exit 2) · monitor (log-only). Point $CODEX_GUARD_RULES at your own file to
override the starter set.
Validate any ruleset (runs each rule's examples as a mini-spec):
python3 bin/check_rules.py rules/starter.rules.json rules/readonly-db.rules.jsonEvery verdict is appended to a hash-chained JSONL at ~/.codex-guard/audit.jsonl
(override with $CODEX_GUARD_AUDIT). Secrets are never stored verbatim — a SHA-256 plus a
redacted preview.
python3 -m guard.audit tail # last 20 verdicts
python3 -m guard.audit verify # prove the chain wasn't altered| Var | Effect |
|---|---|
CODEX_GUARD_RULES |
main ruleset path (default rules/starter.rules.json) |
CODEX_GUARD_AUDIT |
audit-log path |
CODEX_GUARD_DRYRUN=1 |
evaluate + audit, but never block/nudge (observe-only rollout) |
CODEX_GUARD_NUDGE_AS_BLOCK=1 |
promote every nudge to a hard block |
codex-guard reduces preventable damage in Codex sessions; it doesn't secure them. It
catches obvious destructive Bash (rm -rf /, curl | sh, force-push, inline secrets,
bare psql), guards apply_patch writes to secret files, and hard-blocks DB mutations for
a read-only workflow. It does not stop obfuscated commands, prompt injection, a
determined agent, or a locally-compromised machine, and it falls open on its own errors by
design. It's a recoverability-first seatbelt, not a sandbox. Full model in
docs/THREAT_MODEL.md.
python3 -m unittest discover -s tests -vAGPL-3.0-or-later. See LICENSE.
Part of a portfolio of governed, AI-native GTM systems — reference implementations and reusable patterns extracted from a private production stack. In that system this is the recovery-first control surface for the Codex CLI.
Full portfolio map → github.com/kkrlstrm
Works with:
- codex-logger — supplies the telemetry
- agent-guard — the Claude Code sibling; the engine ports unchanged