Philosophy: AI Assists, Humans Decide
Codex CLI is built on a single, non-negotiable principle: the human is always in control. The agent can read your code, reason about your architecture, and propose changes—but it cannot do anything consequential without your explicit approval. This is not a limitation; it is the core design philosophy.
Every dangerous operation passes through multiple layers of defense. These layers are independent and composable: even if one fails, the others hold. Think of it as a medieval castle—moat, drawbridge, walls, archers, and a keep. An attacker must breach all of them.
The Codex safety model follows a "deny by default" philosophy. If the system is uncertain whether an action is safe, it asks. If it cannot ask, it refuses. Silence is never interpreted as consent.
The Six Layers of Defense
Hover over each ring to explore the defense layers. The innermost ring is the most critical—the last line of defense closest to the operating system.
Layer 1: ExecPolicy DSL
The first line of defense is a declarative policy language that determines what commands the agent can execute. Before any tool invocation, the system checks the command against a stack of policy files. This is policy as code—auditable, versionable, and composable.
Three Policy Files, Loaded in Order
Policies are loaded and merged in a strict hierarchy. Each successive layer can only tighten restrictions—a project policy can never grant more permissions than the system defaults allow.
- System defaults — Baked into the binary. Denies known-dangerous operations like
rm -rf /and:(){ :|:& };: - User policy — From
~/.codex/policy.toml. Personal preferences like auto-approvingcargo build - Project policy — From
.codex/policy.tomlin the repo root. Team-wide rules like forbiddingnpm publish
Pattern Matching Rules
Each policy rule matches commands using either prefix or exact matching. A rule maps a pattern to one of three decisions:
# .codex/policy.toml
[[rules]]
pattern = "cargo build"
match = "prefix"
action = "allow" # Auto-approved, no prompt
[[rules]]
pattern = "docker push"
match = "prefix"
action = "prompt" # Requires human approval
[[rules]]
pattern = "rm -rf /"
match = "exact"
action = "forbidden" # Denied, always
The Amendment API: "Yes, and Remember"
When a user approves a prompted command, the system can amend the policy dynamically. This is the "yes and remember" flow—the user's approval is persisted into the user policy file so the same command is auto-approved next time. The policy grows smarter over time without sacrificing safety.
The three-tier decision model (Allow / Prompt / Forbidden) ensures that there is no ambiguity. Every command falls into exactly one bucket, and the system never guesses.
Layer 2: The Approval Chain
When a command falls into the Prompt tier, the user sees a rich approval dialog. This is not a simple yes/no—it's a three-question model designed to give the human full context for an informed decision.
The Three Questions
- What — The exact command to be executed, character for character
- Why — The agent's reasoning: what it hopes to accomplish
- Implications — Side effects, file modifications, network calls, potential risks
The full turn context is shown during approval: the user can see the entire conversation history, the agent's chain of thought, and the specific tool call that triggered the prompt.
Interactive Approval Dialog
Click the buttons below to see what happens with each choice:
docker push myapp:latest registry.prod.internal
Push the newly built container image to the production registry as part of the deployment pipeline.
This will overwrite the :latest tag in the production registry. Any service pulling :latest will receive this image on next restart.
docker push is now auto-approved for this user.
Layer 3: Lifecycle Hooks
Hooks are event listeners that fire at critical moments in the agent's lifecycle. They let you plug in custom logic—logging, auditing, notifications, or even circuit breakers—without modifying the agent itself.
Three Hook Events
- SessionStart — Fires when a new Codex session begins. Use it to set up logging, validate environment, or check credentials.
- AfterToolUse — Fires after every tool invocation. Receives a rich JSON payload with the tool name, arguments, output, duration, and exit code.
- AfterAgent — Fires when the agent completes its turn. Receives the full conversation state and final output.
Rich Structured Payloads
{
"event": "AfterToolUse",
"tool": "shell",
"command": "cargo test --release",
"exit_code": 0,
"duration_ms": 4230,
"stdout_lines": 47,
"stderr_lines": 2,
"working_dir": "/home/user/myproject",
"timestamp": "2025-03-15T14:22:08Z"
}
Custom Hook Example
Hooks are configured in config.toml and can call any executable. Here is a Python hook that logs every shell command to a file:
#!/usr/bin/env python3
# hooks/audit_log.py
import json, sys, datetime
def main():
payload = json.loads(sys.stdin.read())
if payload["event"] != "AfterToolUse":
return
entry = {
"time": datetime.datetime.utcnow().isoformat(),
"cmd": payload.get("command", ""),
"exit": payload.get("exit_code"),
"ms": payload.get("duration_ms"),
}
with open("codex_audit.jsonl", "a") as f:
f.write(json.dumps(entry) + "\n")
if __name__ == "__main__":
main()
# config.toml
[[hooks]]
event = "AfterToolUse"
command = "python3 hooks/audit_log.py"
Hooks are designed to fail gracefully. If a hook crashes, times out, or returns a non-zero exit code, the agent logs a warning and continues. Hooks never block the main agent loop. Safety features should not become availability risks.
Layer 4: Secret Detection
The agent continuously scans its own inputs and outputs for patterns that look like secrets. This is a passive detection layer—it warns without blocking, because false positives in secret detection can halt legitimate workflows.
Pattern Library
The detection engine matches against a curated set of patterns:
sk_live_*,sk_test_*— Stripe API keysghp_*,gho_*— GitHub personal/OAuth tokensmongodb+srv://— Database connection strings with credentialsAKIA*— AWS access key IDs-----BEGIN RSA PRIVATE KEY-----— Private keys- High-entropy base64 strings exceeding a length threshold
Redaction, Not Blocking
When a secret is detected, Codex redacts it from logs and hook payloads. The command still executes, but any observability output replaces the secret with [REDACTED]. This ensures that audit logs and hook data never leak credentials, even if the command itself legitimately needs them.
// Hook payload after redaction
{
"command": "curl -H 'Authorization: Bearer [REDACTED]'",
"exit_code": 0
}
// Terminal log
⚠ Secret detected in command output (line 14).
Pattern: AWS Access Key (AKIA...)
Action: Redacted in logs. Command was not blocked.
Layer 5: Process Hardening
Before the Rust binary even reaches main(), a set of process-level security measures are applied. These use the ctor crate—Rust's equivalent of __attribute__((constructor))—to run hardening code at load time.
What Gets Locked Down
- Core dumps disabled —
setrlimit(RLIMIT_CORE, 0)prevents memory dumps that could leak secrets - ptrace disabled —
prctl(PR_SET_DUMPABLE, 0)prevents debuggers from attaching to the process - LD_PRELOAD / DYLD_INSERT_LIBRARIES removed — Environment variables that allow library injection are stripped before any code runs
- Restrictive file permissions — Configuration files, policy files, and log files are created with
0600permissions (owner read/write only)
// Runs before main() via the ctor crate
#[ctor::ctor]
fn harden_process() {
// Disable core dumps
unsafe {
libc::setrlimit(libc::RLIMIT_CORE, &libc::rlimit {
rlim_cur: 0,
rlim_max: 0,
});
}
// Strip dangerous env vars
for var in &["LD_PRELOAD", "DYLD_INSERT_LIBRARIES"] {
std::env::remove_var(var);
}
}
If hardening runs during main(), there is a window between process start and hardening where an attacker could inject code via LD_PRELOAD. By using ctor, the hardening runs at shared-library load time—before any application code, including main(), executes.
Layer 6: Shell Escalation Detection
The final layer watches for privilege escalation attempts in shell commands. When the agent constructs a command that begins with sudo, su, or doas, special handling kicks in.
Escalation Policy
- Detection — Commands are parsed for escalation prefixes before execution
- Automatic promotion to Prompt tier — Even if the base command is in the Allow list, adding
sudoforces a human approval - Timeout protection — If
sudoprompts for a password and the agent cannot provide one, a 5-second idle timeout automatically cancels the command. This prevents the agent from hanging indefinitely on a password prompt.
# Agent attempts:
$ sudo systemctl restart nginx
⚠ Escalation detected: sudo
Policy override: Promoted to PROMPT tier
Waiting for human approval...
✕ Denied by user policy: sudo commands forbidden in this project
Agent will attempt alternative approach.
The timeout protection is especially important because a hanging sudo prompt is invisible to the agent—it sees no stdout, no stderr, just silence. The 5-second timer treats silence as a signal that the command requires interactive input the agent cannot provide.
Layered Defense in Practice
Let's trace a real scenario: the user asks Codex to "Deploy my app to production." The agent breaks this into three commands. Watch how each command hits a different layer of the defense system.
Click each step to expand the details:
Build the production binary. Matched by user policy prefix rule.
Policy check: cargo build matches prefix rule with action allow
Result: Executed immediately. No prompt shown. Exit code 0.
Hooks fired: AfterToolUse → audit_log.py recorded the command.
Push container image. Requires explicit human approval.
Policy check: docker push matches prefix rule with action prompt
Approval dialog: User sees the three-question model (What/Why/Implications)
User chose: "Yes & Remember" → Policy amended, docker push auto-approved next time
Secret scan: Registry URL checked for embedded credentials. Clean.
Restart the service. Blocked by escalation detection + project policy.
Escalation detected: sudo prefix found, promoted to Prompt tier
Policy check: Project policy forbids all sudo commands
Result: Denied. Agent receives error and proposes alternative: "Shall I create a systemd user service instead?"
Hooks fired: AfterToolUse with exit_code: null and denied: true
Composing Policies
The real power of the policy system is composition. Three independent policy files merge into a single effective policy at runtime. The merge follows the principle of least privilege: when two rules conflict, the more restrictive one wins.
rm -rf / → forbidden* → prompt (default for unknown commands)
cargo build → allowcargo test → allowdocker push → allow (remembered)
docker push → prompt (overrides user allow!)npm publish → forbiddensudo * → forbidden
rm -rf / → forbidden (system)cargo build → allow (user)cargo test → allow (user)docker push → prompt (project overrides user!)npm publish → forbidden (project)sudo * → forbidden (project)* → prompt (system default)
Notice how the project policy overrides the user's remembered docker push allow with prompt. The project maintainers decided that pushing containers should always require approval in this repository, regardless of individual user preferences. The more restrictive rule wins.
Key Takeaways
- Human-in-the-loop is non-negotiable. The agent proposes; the human disposes. No consequential action happens without explicit approval.
- Policy is code. The ExecPolicy DSL is declarative, auditable, and version-controlled. Teams can review policy changes in pull requests.
- Three tiers, no ambiguity. Every command is either Allow, Prompt, or Forbidden. The system never guesses.
- Defense in depth. Six independent layers mean that no single failure compromises the entire system. Policy, approval, hooks, secrets, hardening, and escalation detection each cover different threat vectors.
- Policies compose with least privilege. System + User + Project policies merge, and the most restrictive rule always wins.
- Hooks extend without modifying. Custom audit logging, notifications, and circuit breakers plug in via lifecycle hooks without touching agent code.
- Hardening happens before main(). Process-level security (core dumps, ptrace, library injection) is applied at load time, closing the window for attacks before any application logic runs.
In the next and final article, we will look at the extension points that let you build on top of Codex CLI—custom tools, plugin architectures, and the patterns that turn a coding agent into a platform.