Chock.sh Build your plugin

Agents

OpenAI Codex CLI

OpenAI Codex CLI and chock: install methods and steps, what it can enforce, and the evidence behind it.

Install methods: plugin, sync. Agentseam tier block+rewrite, grade best-effort before a tool runs.

Install methods

  • Plugin: chock install --client codex (ask the user before running it)
  • Repo hooks: chock sync --repo . --agents codex writes .codex/hooks.json (ask the user before running it)

Install steps

Codex (codex)

Tested

The command (ask the user before running it: it builds a plugin of hooks that change how their agent behaves):

chock install --selection <code> --client codex --apply

With --apply, chock runs these steps for Codex:

  1. codex plugin marketplace add ~/.chock/marketplace/codex

Then install chock-guardrails from the chock-local marketplace in /plugins, and review and trust its hooks in /hooks: until you do, they do not run.

What it can enforce

Tier
block+rewrite
Grade before a tool runs
best-effort
Fail mode before a tool runs
open
Basis
live-run-partial
Recorded
2026-08-28

Events

EventCan blockCan rewriteFail mode
pre_toolyesyesopen
post_toolnonoopen
prompt_submityesnoopen
session_startnonoopen
session_endnonoopen
pre_compactnonoopen
stopyesnoopen
subagent_startnonoopen
subagent_stopnonoopen

Ask support

Answers an ask with: deny. Honours ask: no. Evidence: tested (tests/test_guard_fail_to_ask.py::test_a_crashed_guard_asks_rather_than_allowing).

Hook timeout

Default 600 seconds, set by the timeout field in s.

Vendor source

Test status

  • Plugin (codex): tested.
  • Repo hooks: untested. A test of the plugin says nothing about repo hooks.
  • Through the plugin install, at hook point plugin-hook: Codex Desktop, Windows, 2026-08-24. live probe: a hook-JSON deny blocked a matched command (chock 6b4d400)

Known limits

Claude-family decision shape (hookSpecificOutput.permissionDecision) and Claude-family PascalCase event names (verified against hook_config.rs/schema.rs; an earlier version of this note wrongly said camelCase, sourced from the App Server's separate IDE-facing protocol rather than the CLI hook dialect this adapter speaks). Told apart from Claude Code by turn_id: permission_mode and model were also believed exclusive to Codex and turned out not to be -- a real live-captured Claude Code payload carries permission_mode too, and model cannot discriminate either since Cursor's base hook schema also sends it on every event -- so turn_id is the only one of the three still unrefuted by a real payload. Deny is sent as JSON with exit 0: on Windows Codex wraps hooks in powershell -Command, which collapses exit 2 into 1, so an exit-code deny does not survive that platform. That same PowerShell wrapper will not RUN a command line beginning with a quoted path -- it parses as a string expression, not an invocation -- so installs emit Codex's own commandWindows override carrying the & call operator (witnessed live 2026-08-28: without it every hook failed with exit 1 and never executed). Three response dialects, one per event group, because every per-event output struct is deny_unknown_fields and rejects the whole response over a foreign key: pre_tool takes hookSpecificOutput.permissionDecision, prompt_submit and stop take a top-level {decision: block, reason}, and the observation-only events accept no verdict at all and get silence. A bare allow is silence everywhere -- permissionDecision:allow is rejected unless it carries updatedInput, and a rejected response fails OPEN. ask is likewise unsupported and degrades to a deny with an explanatory reason. Live capture 2026-08-28 (36 payloads) settled the write vocabulary: Codex sends exactly two tool names, Bash and apply_patch, and BOTH carry only tool_input.command -- apply_patch is the file-writing tool and the patch rides in that string, so event.path and event.content are None on a real write and a content policy has to gate on event.command. The file_path/content/new_string/edits vocabulary previously read here was Claude Code's, copied over on an assumption, and no Codex payload has ever carried it. The same capture showed SessionStart carries no turn_id (it is not turn-scoped) and nothing else separates it from Claude Code's, so detect() now declines on it rather than answering in the wrong dialect.

Built from engine 5a4b6287a46e and catalog 1e60d211c1bb.