Codex-CLI-specific notes for the ccgate codex hook.
- Codex hooks require
[features] hooks = true. See OpenAI's Codex hooks docs for the upstream payload schema. - Tool-agnostic. Codex hooks fire for Bash,
apply_patch, MCP tool calls, and other surfaces. ccgate classifies bytool_name+ the fulltool_inputJSON, not by tool kind alone.
Codex CLI lookup order (per OpenAI's Codex hooks docs):
~/.codex/hooks.json~/.codex/config.toml<repo>/.codex/hooks.json(only when the project's.codex/layer is trusted)<repo>/.codex/config.toml(same trust requirement)
Layers are additive -- a hook registered globally and a hook registered project-local both fire.
{
"hooks": {
"PermissionRequest": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "ccgate codex",
"statusMessage": "ccgate evaluating request"
}
]
}
]
}
}"matcher": "" makes ccgate evaluate every PermissionRequest event Codex emits (Bash, apply_patch, MCP tool calls, ...). Restrict by tool name pattern if you only want a subset.
If you want to keep hooks alongside the rest of your Codex config:
[features]
hooks = true # Codex hooks live behind this feature flag.
[[hooks.PermissionRequest]]
matcher = ""
[[hooks.PermissionRequest.hooks]]
type = "command"
command = "ccgate codex"
statusMessage = "ccgate evaluating request"Project-local <repo>/.codex/{hooks.json,config.toml} only loads when the project is trusted. For an in-tree dev build of ccgate, drop a project-local hooks file (untracked) and point it at go run:
go run build cache makes second-onwards invocations fast. Dotfiles-managed ~/.codex/config.toml is not touched.
ccgate forwards the full tool_input JSON to the LLM verbatim, so MCP arguments and apply_patch hunk metadata reach the classifier untouched even when ccgate has no typed field for them. The metrics layer pulls a small parsed view (command / description / file_path / path / pattern) for the JSONL but never strips the raw payload from the LLM message.
Fields ccgate reads from the Codex HookInput:
session_idtranscript_path(path only; ccgate does not parse the transcript JSONL)cwdhook_event_namemodel(the AI side's model, e.g.gpt-5)turn_idtool_name(Bash,apply_patch,mcp__<server>__<tool>, ...)tool_input(typed view)tool_input_raw(the original JSON payload, forwarded verbatim — the primary surface for inspectingapply_patchhunks and MCP arguments)referenced_paths(best-effort path extraction fromtool_input. Supported forBash;apply_patchand MCP fall back to readingtool_input_rawdirectly.)
The Codex system prompt tells the LLM to judge from tool_name + tool_input + tool_input_raw + cwd, so it does not invent context that isn't present in the HookInput.
| Aspect | Value |
|---|---|
| Tool surface | Bash, apply_patch, MCP (mcp__<server>__<tool>). Codex hooks fire for every PermissionRequest event regardless of tool kind. |
| State path | $XDG_STATE_HOME/ccgate/codex/ (falls back to ~/.local/state/ccgate/codex/ when unset). |
| Project-local config | {repo_root}/.codex/ccgate.local.jsonnet (untracked-only, project-trust required). |
Run ccgate codex init | less to read the full allow / deny / environment guidance compiled into the binary. For how to extend or replace these defaults, see docs/rule-tuning.md.