A session can need local housekeeping before its first model response. Codex hooks attach that work to lifecycle events, and a trusted SessionStart hook can record when a new session starts.
User hooks follow the active Codex home and apply across projects, independently of project trust. Each command handler receives JSON describing its event, which lets a startup log record the event name and start source supplied by Codex.
Hooks execute local programs with the user's permissions outside the Codex sandbox. A Unix shell, Python 3, and a signed-in Codex CLI are required; a hook deserves the same source review as any other program run under that account.
Steps to add a Codex hook:
- Resolve the active user configuration directory.
$ codex_home="${CODEX_HOME:-$HOME/.codex}"With the default home, the files are:
~/.codex/hooks/startup.py ~/.codex/hooks.json
With a custom CODEX_HOME, they are:
$CODEX_HOME/hooks/startup.py $CODEX_HOME/hooks.json
New CLI sessions must use the same home.
- Create the hook script directory.
$ mkdir -p "$codex_home/hooks"
- Open the active home's hooks/startup.py in your text editor as an existing file or a new file.
The default path is ~/.codex/hooks/startup.py. If a different program already uses that filename, choose an unused script name and use that same name in the handler command.
- Enter the event logger in startup.py.
- startup.py
import json import os import sys from pathlib import Path event = json.load(sys.stdin) home = Path(os.environ.get("CODEX_HOME", Path.home() / ".codex")) with (home / "hook-events.log").open("a") as log: log.write(f'{event["hook_event_name"]} {event["source"]}\n')
The script reads the actual hook input and appends only the event name and source. It preserves earlier log entries and does not store prompts, account details, or session identifiers.
- Save startup.py in your text editor.
- Restrict access to the logger to its owner.
$ chmod 700 "$codex_home/hooks/startup.py"
- Open the active home's hooks.json in your text editor as an existing file or a new file.
The default path is ~/.codex/hooks.json; a custom home uses $CODEX_HOME/hooks.json. Existing handlers and other settings belong in the saved file.
- Add the startup handler to hooks.json using the block for its current structure.
For a missing file, paste this complete JSON document.
- hooks.json
{ "hooks": { "SessionStart": [ { "matcher": "^startup$", "hooks": [ { "type": "command", "command": "python3 \"${CODEX_HOME:-$HOME/.codex}/hooks/startup.py\"", "statusMessage": "Recording session start", "timeout": 10 } ] } ] } }
If the existing "hooks" object has no "SessionStart" key, insert this member immediately before that object's closing brace. A comma belongs after the preceding member's value when one exists; there is no comma after the final member.
"SessionStart": [ { "matcher": "^startup$", "hooks": [ { "type": "command", "command": "python3 \"${CODEX_HOME:-$HOME/.codex}/hooks/startup.py\"", "statusMessage": "Recording session start", "timeout": 10 } ] } ]If "SessionStart" already exists, insert this matcher group immediately before that array's closing bracket instead. A comma separates it from the preceding group, if any; the final group has no trailing comma. Other groups and their handlers stay in place.
{ "matcher": "^startup$", "hooks": [ { "type": "command", "command": "python3 \"${CODEX_HOME:-$HOME/.codex}/hooks/startup.py\"", "statusMessage": "Recording session start", "timeout": 10 } ] }If an existing file has no top-level "hooks" member, add the "hooks": { ... } member from the complete document inside its outer braces, preserving its other members and separating them with a comma. There must be only one top-level "hooks" key and one "SessionStart" key inside it. If that startup group already contains this command, no second copy is needed. The anchored matcher selects fresh startup sessions; the timeout is in seconds.
- Save hooks.json in your text editor.
- Validate the hook file syntax.
$ python3 -m json.tool "$codex_home/hooks.json" >/dev/null
A successful syntax check returns silently. A JSON error must be corrected before the hook can load. JSON parsing alone does not detect every duplicate key.
- Inspect any existing startup log before testing the hook.
$ cat "$codex_home/hook-events.log"
The default log path is:
~/.codex/hook-events.log
With a custom CODEX_HOME, the log path is:
$CODEX_HOME/hook-events.log
An absent file is normal before the first logged event. Existing entries provide the history against which the next appended line can be recognized.
- Start a new CLI session with a short greeting request.
$ codex "Reply with one short greeting."
The request initializes the session; opening an idle CLI alone does not trigger the startup hook. Normal sandbox and approval settings can remain in place.
- Select Continue without trusting if the initial hook review warning appears.
Trusting a hook permits its local command to execute with your account's permissions. The greeting can proceed with the new hook skipped while its definition and script await inspection.
- Open the hook browser after the greeting completes.
/hooks
The SessionStart row identifies the installed hooks requiring review. Other existing handlers may also appear.
- Select SessionStart to display its handler details.
- Select Recording session start in the handler list.
With more than one handler installed, the arrow keys select the one whose details appear below the list.
- Inspect the Recording session start handler against the saved definition.
The command must point to the logger just saved, with SessionStart as the event, ^startup$ as the matcher, and a 10-second timeout. Its source must be the active user hooks.json file. Other handlers require their own review.
- Inspect the saved hooks/startup.py source in your text editor before trusting its handler.
Its contents must still match the event logger above, with append mode for the log and no additional commands or data collection. The hook browser shows the command definition, not the contents of the external script.
- Press t with Recording session start selected to trust that reviewed handler.
The handler now shows Trusted and an enabled [x] marker. Trust is tied to the current definition hash. A changed definition requires review again; edits to an external script also deserve a fresh source review.
- Return to the composer with Esc twice.
The first press returns to the event list; the second closes the browser.
- Exit the current session.
/quit
- Start a fresh session with the same greeting request.
$ codex "Reply with one short greeting."
The trusted startup hook runs when this new session initializes. If another handler still requires review, Continue without trusting leaves that handler untrusted. The greeting wording can vary.
- Exit the new session after the greeting completes.
/quit
- Read the newly appended startup event in the active home's log.
$ cat "$codex_home/hook-events.log" ##### snipped ##### SessionStart startup
The new bottom line is SessionStart startup; earlier entries remain above it. The snipped marker represents any older history. If the file is absent or no line was appended, the hook did not record a startup; hooks.json syntax, the script path, the hook feature setting, and the Trusted and enabled states in /hooks determine whether it can run. Hooks are enabled by default. The definition and logger remain installed for subsequent sessions.