Secure OpenAI Codex
OpenAI Codex already fires hooks for prompts and tool calls. Arcjet uses those hooks to enforce your policies and record the session. You don’t change how developers work.
What is Arcjet?
Arcjet is the AI agent runtime security platform. Discover the agents running in your organization, enforce policy across every action, prompt, and tool call, and keep the evidence to prove what happened. Detect prompt injection, authorize agent tool calls, redact PII, and block bots and abuse.Before you start
Section titled “Before you start”Create a free Arcjet account then use the key to authenticate the Arcjet hooks. It is semi-secret, but can be distributed to multiple devices and team members through environment variables or configuration management.
Codex runs hooks as commands, not as native HTTP handlers. The install is a
small script that reads the hook payload from standard input, posts it to
Arcjet, and writes Arcjet’s response to standard output. The machine that
runs Codex needs curl.
Choose where to deploy
Section titled “Choose where to deploy”Codex has two channels for organization policy. Managed requirements are admin-enforced and users can’t override them. Hooks also load from user, project, and plugin files. Managed hooks sit above every hook a developer can write.
| Mechanism | Where the files go | Reaches | Removable by a developer |
|---|---|---|---|
System requirements.toml | /etc/codex/requirements.toml (Linux, macOS) or %ProgramData%\OpenAI\Codex\requirements.toml (Windows), plus scripts under managed_dir | That device: CLI, app, and IDE extension | Local administrator only |
| MDM managed preferences | macOS com.openai.codex requirements_toml_base64 | The same surfaces, redeployable on a schedule | Local administrator only |
| Cloud-managed requirements | ChatGPT Business or Enterprise managed-config page | Every session that signs in with an eligible ChatGPT credential | Only by switching provider |
| Repository settings | .codex/hooks.json or .codex/config.toml, committed | Sessions in that repository, after the project .codex/ layer is trusted | Yes |
If your developers use Codex on the web, read Cloud sessions.
Install the hook script
Section titled “Install the hook script”Save this script next to the hook configuration. In a repository, put it at
.codex/hooks/arcjet-hook.sh. For a managed install, put it at the absolute
path you set as managed_dir.
#!/bin/sh# Usage: arcjet-hook.sh VENDOR EVENT# Reads one hook payload from stdin and posts it to Arcjet.# On success: print the body and exit 0.# On any transport failure, non-2xx, or non-JSON 2xx: print the# vendor denial shape and exit 2.
vendor=$1event=$2principal=${USER:-${USERNAME:-}}
deny() { case "${vendor}:${event}" in codex:pre-tool-use) printf '%s\n' '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":"arcjet-hook"}}' ;; codex:permission-request) printf '%s\n' '{"hookSpecificOutput":{"hookEventName":"PermissionRequest","decision":{"behavior":"deny","message":"arcjet-hook"}}}' ;; codex:user-prompt-submit) printf '%s\n' '{"decision":"block","reason":"arcjet-hook"}' ;; *) printf '%s\n' '{}' ;; esac exit 2}
tmp=$(mktemp) || denytrap 'rm -f "$tmp"' EXITcode=$(curl -sS --max-time 5 -o "$tmp" -w '%{http_code}' -X POST \ "https://decide.arcjet.com/v1/agent-hooks/${vendor}?event=${event}" \ -H "Authorization: Bearer ${ARCJET_KEY}" \ -H "Content-Type: application/json" \ -H "X-Arcjet-Principal: ${principal}" \ --data-binary @-) || denycase "$code" in 2??) ;; *) deny ;;esachead=$(dd if="$tmp" bs=1 count=1 2>/dev/null) || denycase "$head" in \{|\[) ;; *) deny ;;esaccat "$tmp"exit 0Make the script executable. On Windows, save a sibling arcjet-hook.ps1 and
point command_windows at it:
param([string]$Vendor, [string]$Event)$principal = if ($env:USERNAME) { $env:USERNAME } else { $env:USER }function Deny { switch ("$Vendor`:$Event") { "codex:pre-tool-use" { Write-Output '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":"arcjet-hook"}}' } "codex:permission-request" { Write-Output '{"hookSpecificOutput":{"hookEventName":"PermissionRequest","decision":{"behavior":"deny","message":"arcjet-hook"}}}' } "codex:user-prompt-submit" { Write-Output '{"decision":"block","reason":"arcjet-hook"}' } default { Write-Output '{}' } } exit 2}try { $body = [Console]::In.ReadToEnd() $headers = @{ Authorization = "Bearer $env:ARCJET_KEY" "Content-Type" = "application/json" "X-Arcjet-Principal" = "$principal" } $response = Invoke-WebRequest -Method Post -TimeoutSec 5 ` -Uri "https://decide.arcjet.com/v1/agent-hooks/$Vendor`?event=$Event" ` -Headers $headers -Body $body if ($response.StatusCode -lt 200 -or $response.StatusCode -ge 300) { Deny } $content = $response.Content if ($content -notmatch '^\s*[\{\[]') { Deny } Write-Output $content} catch { Deny}arcjet-hook reads stdin, POSTs it to
https://decide.arcjet.com/v1/agent-hooks/<vendor>?event=<event> with
Authorization: Bearer $ARCJET_KEY, prints the body, and exits 0. On
failure it prints the denial shape for that vendor and event and exits 2.
Exit 2 is what Codex treats as a deny on PreToolUse and
UserPromptSubmit. A wrong key denies every Codex prompt and tool call.
The script does not log the body.
Install the hooks
Section titled “Install the hooks”This template is a complete setup. It installs every enforcement point
plus the recorded events worth keeping. It omits matcher, so Arcjet sees
every tool Codex reports on PreToolUse and PermissionRequest. Replace
the script path if you install the files somewhere else.
PermissionRequest is the other Tool call event. It doesn’t fire for
calls that never ask for approval, so it isn’t the control to rely on.
PreToolUse is.
Codex has no model-switch hook. A model-list policy refuses the prompt and the tool call while a model that isn’t in the list is selected. For more information about writing the list, see Allowed models.
Show .codex/hooks.json
{ "hooks": { "PreToolUse": [ { "hooks": [ { "type": "command", "command": "sh .codex/hooks/arcjet-hook.sh codex pre-tool-use", "commandWindows": "powershell -NoProfile -File .codex/hooks/arcjet-hook.ps1 -Vendor codex -Event pre-tool-use", "timeout": 5 } ] } ], "PermissionRequest": [ { "hooks": [ { "type": "command", "command": "sh .codex/hooks/arcjet-hook.sh codex permission-request", "commandWindows": "powershell -NoProfile -File .codex/hooks/arcjet-hook.ps1 -Vendor codex -Event permission-request", "timeout": 5 } ] } ], "UserPromptSubmit": [ { "hooks": [ { "type": "command", "command": "sh .codex/hooks/arcjet-hook.sh codex user-prompt-submit", "commandWindows": "powershell -NoProfile -File .codex/hooks/arcjet-hook.ps1 -Vendor codex -Event user-prompt-submit", "timeout": 5 } ] } ], "PostToolUse": [ { "hooks": [ { "type": "command", "command": "sh .codex/hooks/arcjet-hook.sh codex post-tool-use", "commandWindows": "powershell -NoProfile -File .codex/hooks/arcjet-hook.ps1 -Vendor codex -Event post-tool-use", "timeout": 5 } ] } ], "Stop": [ { "hooks": [ { "type": "command", "command": "sh .codex/hooks/arcjet-hook.sh codex stop", "commandWindows": "powershell -NoProfile -File .codex/hooks/arcjet-hook.ps1 -Vendor codex -Event stop", "timeout": 5 } ] } ], "SessionStart": [ { "hooks": [ { "type": "command", "command": "sh .codex/hooks/arcjet-hook.sh codex session-start", "commandWindows": "powershell -NoProfile -File .codex/hooks/arcjet-hook.ps1 -Vendor codex -Event session-start", "timeout": 5 } ] } ], "SessionEnd": [ { "hooks": [ { "type": "command", "command": "sh .codex/hooks/arcjet-hook.sh codex session-end", "commandWindows": "powershell -NoProfile -File .codex/hooks/arcjet-hook.ps1 -Vendor codex -Event session-end", "timeout": 3 } ] } ], "SubagentStart": [ { "hooks": [ { "type": "command", "command": "sh .codex/hooks/arcjet-hook.sh codex subagent-start", "commandWindows": "powershell -NoProfile -File .codex/hooks/arcjet-hook.ps1 -Vendor codex -Event subagent-start", "timeout": 5 } ] } ], "SubagentStop": [ { "hooks": [ { "type": "command", "command": "sh .codex/hooks/arcjet-hook.sh codex subagent-stop", "commandWindows": "powershell -NoProfile -File .codex/hooks/arcjet-hook.ps1 -Vendor codex -Event subagent-stop", "timeout": 5 } ] } ], "PreCompact": [ { "hooks": [ { "type": "command", "command": "sh .codex/hooks/arcjet-hook.sh codex pre-compact", "commandWindows": "powershell -NoProfile -File .codex/hooks/arcjet-hook.ps1 -Vendor codex -Event pre-compact", "timeout": 5 } ] } ] }}The repository template uses a path relative to the project directory
Codex is running in. Don’t wrap it in git rev-parse --show-toplevel:
outside a git working tree that resolves to / and the hook never runs.
For a user-level file at ~/.codex/hooks.json, change each command to
sh ~/.codex/hooks/arcjet-hook.sh codex EVENT.
For a managed install, put the same events in requirements.toml with
absolute script paths. Codex doesn’t distribute the scripts; your MDM or
device-management tooling must install them under managed_dir. Managed
hooks are trusted by policy and can’t be disabled from /hooks.
Show requirements.toml
allow_managed_hooks_only = true
[features]hooks = true
[hooks]managed_dir = "/etc/codex/hooks"windows_managed_dir = 'C:\ProgramData\OpenAI\Codex\hooks'
[[hooks.PreToolUse]][[hooks.PreToolUse.hooks]]type = "command"command = "sh /etc/codex/hooks/arcjet-hook.sh codex pre-tool-use"command_windows = 'powershell -NoProfile -File C:\ProgramData\OpenAI\Codex\hooks\arcjet-hook.ps1 -Vendor codex -Event pre-tool-use'timeout = 5
[[hooks.PermissionRequest]][[hooks.PermissionRequest.hooks]]type = "command"command = "sh /etc/codex/hooks/arcjet-hook.sh codex permission-request"command_windows = 'powershell -NoProfile -File C:\ProgramData\OpenAI\Codex\hooks\arcjet-hook.ps1 -Vendor codex -Event permission-request'timeout = 5
[[hooks.UserPromptSubmit]][[hooks.UserPromptSubmit.hooks]]type = "command"command = "sh /etc/codex/hooks/arcjet-hook.sh codex user-prompt-submit"command_windows = 'powershell -NoProfile -File C:\ProgramData\OpenAI\Codex\hooks\arcjet-hook.ps1 -Vendor codex -Event user-prompt-submit'timeout = 5
[[hooks.PostToolUse]][[hooks.PostToolUse.hooks]]type = "command"command = "sh /etc/codex/hooks/arcjet-hook.sh codex post-tool-use"command_windows = 'powershell -NoProfile -File C:\ProgramData\OpenAI\Codex\hooks\arcjet-hook.ps1 -Vendor codex -Event post-tool-use'timeout = 5
[[hooks.Stop]][[hooks.Stop.hooks]]type = "command"command = "sh /etc/codex/hooks/arcjet-hook.sh codex stop"command_windows = 'powershell -NoProfile -File C:\ProgramData\OpenAI\Codex\hooks\arcjet-hook.ps1 -Vendor codex -Event stop'timeout = 5
[[hooks.SessionStart]][[hooks.SessionStart.hooks]]type = "command"command = "sh /etc/codex/hooks/arcjet-hook.sh codex session-start"command_windows = 'powershell -NoProfile -File C:\ProgramData\OpenAI\Codex\hooks\arcjet-hook.ps1 -Vendor codex -Event session-start'timeout = 5
[[hooks.SessionEnd]][[hooks.SessionEnd.hooks]]type = "command"command = "sh /etc/codex/hooks/arcjet-hook.sh codex session-end"command_windows = 'powershell -NoProfile -File C:\ProgramData\OpenAI\Codex\hooks\arcjet-hook.ps1 -Vendor codex -Event session-end'timeout = 3
[[hooks.SubagentStart]][[hooks.SubagentStart.hooks]]type = "command"command = "sh /etc/codex/hooks/arcjet-hook.sh codex subagent-start"command_windows = 'powershell -NoProfile -File C:\ProgramData\OpenAI\Codex\hooks\arcjet-hook.ps1 -Vendor codex -Event subagent-start'timeout = 5
[[hooks.SubagentStop]][[hooks.SubagentStop.hooks]]type = "command"command = "sh /etc/codex/hooks/arcjet-hook.sh codex subagent-stop"command_windows = 'powershell -NoProfile -File C:\ProgramData\OpenAI\Codex\hooks\arcjet-hook.ps1 -Vendor codex -Event subagent-stop'timeout = 5
[[hooks.PreCompact]][[hooks.PreCompact.hooks]]type = "command"command = "sh /etc/codex/hooks/arcjet-hook.sh codex pre-compact"command_windows = 'powershell -NoProfile -File C:\ProgramData\OpenAI\Codex\hooks\arcjet-hook.ps1 -Vendor codex -Event pre-compact'timeout = 5What the hooks do
Section titled “What the hooks do”Codex launches matching command hooks for an event concurrently, so these entries add one request of latency (typically a few tens of milliseconds).
- The wrapper posts to
https://decide.arcjet.com/v1/agent-hooks/codexwitheventas a query parameter. Policy inputs identify Codex asagent_vendor: openaiandagent_product: codex. PreToolUseandPermissionRequestrun on Tool call.UserPromptSubmitruns on Prompt. Codex honors a prompt denial. Codex doesn’t fire prompt expansion.- Omit
matcher. A matcher that lists onlyBashorapply_patchleaves MCP tools and other local function tools outside the policy. - Codex reports
apply_patch,exec_command,spawn_agent, andupdate_plan. Arcjet maps those tofile_write,shell,agent, andother. It also matches the shared PascalCase names such asBash,Read, andWrite. timeouton every entry. Codex’s default for most hooks is 600 seconds.SessionEndsupports at most 3 seconds.X-Arcjet-Principalattributes a session to a developer. It’s untrusted, but useful extra metadata.$USERis unset on Windows, so the script falls back to$USERNAME.- The hook URLs omit
surface. The same files reach the CLI, app, IDE extension, and cloud, so a hard-codedclimislabels most of that traffic. Arcjet recordsunknownwhen you omit the parameter. Set it only when the hook file is specific to one surface. - Hosted tools such as
WebSearchdon’t use Codex’s local function-tool hook path, soPreToolUsenever sees them.
Project-local hooks load only when the project .codex/ layer is trusted.
Codex skips a non-managed command hook until someone reviews and trusts
the exact definition in /hooks. Managed hooks from
requirements.toml, MDM, or cloud requirements are trusted by policy and
can’t be disabled from the user hook browser.
What Arcjet answers
Section titled “What Arcjet answers”The wrapper prints Arcjet’s JSON to standard output. Codex reads that as the hook result. Echo the body as-is.
| Event | Denial body | Honored |
|---|---|---|
pre-tool-use | { "hookSpecificOutput": { "hookEventName": "PreToolUse", "permissionDecision": "deny", "permissionDecisionReason": "RULE_ID" } } | Yes |
permission-request | { "hookSpecificOutput": { "hookEventName": "PermissionRequest", "decision": { "behavior": "deny", "message": "RULE_ID" } } } | Yes |
user-prompt-submit | { "decision": "block", "reason": "RULE_ID" } | Yes |
An allow is {}. Codex treats permissionDecision: "allow" as a grant
that skips its own permission flow, and can rewrite the call when that
grant carries updatedInput. The denial reason names the rule IDs that
fired and nothing else.
Supply the Arcjet key
Section titled “Supply the Arcjet key”The script reads ARCJET_KEY from the environment. Export it where Codex
runs, or inject it through your configuration-management tooling. Never
commit a literal key.
X-Arcjet-Principal uses $USER or $USERNAME. You don’t need to list
those names in a Codex allowlist; Codex command hooks inherit the process
environment.
Lock it down
Section titled “Lock it down”The last keys in the managed template are the lockdown:
allow_managed_hooks_only = true
[features]hooks = trueallow_managed_hooks_only is valid only in requirements.toml. Putting
it in config.toml doesn’t enable managed-hooks-only mode.
Without allow_managed_hooks_only, a developer’s own hooks run alongside
yours. Pin [features].hooks = true so a local hooks = false can’t turn
the managed entries off.
Cloud sessions
Section titled “Cloud sessions”A cloud session on Codex on the web runs in an environment on a clone of the repository rather than on the developer’s machine.
Deliver the hooks
Section titled “Deliver the hooks”A cloud session reads a committed .codex/hooks.json after the project
layer is trusted, and your organization’s cloud-managed requirements.
Allow the endpoint
Section titled “Allow the endpoint”Codex environments block agent internet access by default. Turn agent
internet access On, add decide.arcjet.com to the domain allowlist,
and allow POST. A methods allowlist that keeps only GET, HEAD, and
OPTIONS blocks the hook.
If you pin sandboxed networking with
experimental_network.managed_allowed_domains_only, add
decide.arcjet.com to experimental_network.allowed_domains as well.
Verify the install
Section titled “Verify the install”-
Run
/hooksin Codex. Confirm the Arcjet entries appear, and trust them if Codex marked a non-managed definition for review. -
Ask Codex to run a harmless command, such as listing a directory.
-
Open the site’s Activity in the Arcjet Console and confirm the session and the tool call appear.
Related
Section titled “Related”- Secure coding agents – the endpoint, the events, and where enforcement stops
- Coding agent policies – the input contract and the starter policies
- Secure Claude Code
- Secure GitHub Copilot
- Secure Cursor
- Block personal coding agent accounts – keep personal Codex and other agents off the managed network