Skip to content

Commit 7df81f8

Browse files
feat: add OpenAI Codex hook support
Adds provider-aware hooks for OpenAI Codex, setup output for ~/.codex/hooks.json, docs, tests, and the 0.67.0 version bump. Closes #191
1 parent 1bbd979 commit 7df81f8

8 files changed

Lines changed: 473 additions & 82 deletions

File tree

README.md

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -36,12 +36,13 @@ uvx agent-strace replay
3636

3737
## Quick start
3838

39-
**Option 1: Claude Code hooks** — captures everything (prompts, responses, every tool call)
39+
**Option 1: CLI hooks** — captures prompts, responses, and hook-visible tool calls
4040

4141
```bash
42-
agent-strace setup # prints hooks config — add to ~/.claude/settings.json
43-
agent-strace list # list sessions
44-
agent-strace replay # replay the latest
42+
agent-strace setup # Claude Code hooks for ~/.claude/settings.json
43+
agent-strace setup --cli codex # OpenAI Codex hooks for ~/.codex/hooks.json
44+
agent-strace list # list sessions
45+
agent-strace replay # replay the latest
4546
```
4647

4748
Full config and JSON: [docs/setup.md](docs/setup.md)
@@ -138,7 +139,7 @@ Install **agent-strace** from the [Extensions panel](https://open-vsx.org/extens
138139

139140
```bash
140141
pip install agent-strace # 1. install
141-
agent-strace setup # 2. add hooks to Claude Code
142+
agent-strace setup # 2. add hooks to Claude Code or use --cli codex for Codex
142143
# 3. open project in VS Code — extension activates when .agent-traces/ exists
143144
# 4. start Claude Code — status bar appears immediately
144145
```

docs/commands.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,9 @@ Capture an MCP HTTP/SSE server session. Listens on `--port` (default: 3100) and
2727

2828
### `setup`
2929
```
30-
agent-strace setup [--no-redact] [--global]
30+
agent-strace setup [--cli claude|codex|all] [--no-redact] [--global]
3131
```
32-
Print Claude Code hooks config JSON for `~/.claude/settings.json`. Use `--global` to scope hooks to all projects (default scopes to the current project). Secret redaction is enabled by default; use `--no-redact` only for trusted local traces.
32+
Print hooks config JSON for supported agent CLIs. `--cli claude` prints Claude Code settings JSON for `~/.claude/settings.json`; `--cli codex` prints OpenAI Codex hooks JSON for `~/.codex/hooks.json`; `--cli all` prints both. Secret redaction is enabled by default; use `--no-redact` only for trusted local traces.
3333

3434
### `import`
3535
```

docs/integrations.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,19 @@ Each integration is an optional extra — the core package stays dependency-free
4848

4949
---
5050

51+
## Agent CLI hooks
52+
53+
Use setup-generated hooks when the agent CLI has its own lifecycle hook system.
54+
55+
| CLI | Setup | What's traced |
56+
|---|---|---|
57+
| Claude Code | `agent-strace setup --cli claude` | Session start/end, user prompts, assistant responses, tool calls/results |
58+
| OpenAI Codex | `agent-strace setup --cli codex` | Session start, user prompts, assistant responses, `PreToolUse`/`PostToolUse` tools |
59+
60+
Both paths write the same event stream under `.agent-traces/`, so replay, timeline, explain, why, watch, export, and audit commands work the same way after capture.
61+
62+
---
63+
5164
## OpenAI Agents SDK
5265

5366
```python

docs/setup.md

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,20 @@ Three ways to capture agent sessions. Pick the one that matches your agent.
44

55
---
66

7-
## Option 1: Claude Code hooks (recommended)
7+
## Option 1: CLI hooks (recommended)
88

99
Captures everything: user prompts, assistant responses, and every tool call (Bash, Edit, Write, Read, Agent, Grep, Glob, WebFetch, WebSearch, all MCP tools).
1010

1111
```bash
12-
# Generate and apply hooks config
12+
# Generate Claude Code hooks config
1313
agent-strace setup
1414

15+
# Generate OpenAI Codex hooks config
16+
agent-strace setup --cli codex
17+
18+
# Generate both hook configs
19+
agent-strace setup --cli all
20+
1521
# For all projects (global config)
1622
agent-strace setup --global
1723

@@ -43,6 +49,24 @@ agent-strace replay # replay the latest
4349
agent-strace explain # plain-English summary
4450
```
4551

52+
### OpenAI Codex hooks
53+
54+
`agent-strace setup --cli codex` prints hooks JSON for `~/.codex/hooks.json`:
55+
56+
```json
57+
{
58+
"hooks": {
59+
"SessionStart": [{ "matcher": "startup|resume|clear|compact", "hooks": [{ "type": "command", "command": "agent-strace hook --provider codex session-start" }] }],
60+
"UserPromptSubmit": [{ "hooks": [{ "type": "command", "command": "agent-strace hook --provider codex user-prompt" }] }],
61+
"PreToolUse": [{ "matcher": ".*", "hooks": [{ "type": "command", "command": "agent-strace hook --provider codex pre-tool" }] }],
62+
"PostToolUse": [{ "matcher": ".*", "hooks": [{ "type": "command", "command": "agent-strace hook --provider codex post-tool" }] }],
63+
"Stop": [{ "hooks": [{ "type": "command", "command": "agent-strace hook --provider codex stop" }] }]
64+
}
65+
}
66+
```
67+
68+
Codex sends one JSON object to each command hook on stdin. agent-strace records the common Codex fields (`session_id`, `turn_id`, `tool_use_id`, `tool_name`, `tool_input`, `tool_response`, `prompt`, and `last_assistant_message`) into the same `.agent-traces/` session store used by Claude Code.
69+
4670
### Import existing sessions
4771

4872
Already ran sessions without hooks? Import from Claude Code's native JSONL logs:

src/agent_trace/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
"""agent-trace: strace for AI agents."""
22

3-
__version__ = "0.66.0"
3+
__version__ = "0.67.0"

src/agent_trace/cli.py

Lines changed: 76 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -450,16 +450,18 @@ def cmd_stats(args: argparse.Namespace) -> int:
450450
return 0
451451

452452

453-
def cmd_setup(args: argparse.Namespace) -> None:
454-
"""Generate Claude Code hooks configuration."""
453+
def _hook_command_prefix(args: argparse.Namespace, provider: str = "claude") -> str:
455454
redact_env = ""
456455
if args.no_redact:
457456
redact_env = "AGENT_TRACE_NO_REDACT=1 "
458457
elif args.redact:
459458
redact_env = "AGENT_TRACE_REDACT=1 "
459+
provider_arg = "" if provider == "claude" else f"--provider {provider} "
460+
return f"{redact_env}agent-strace hook {provider_arg}".rstrip()
460461

461-
cmd_prefix = f"{redact_env}agent-strace hook"
462462

463+
def _claude_hooks_config(args: argparse.Namespace) -> dict:
464+
cmd_prefix = _hook_command_prefix(args, provider="claude")
463465
config = {
464466
"hooks": {
465467
"UserPromptSubmit": [{
@@ -488,16 +490,69 @@ def cmd_setup(args: argparse.Namespace) -> None:
488490
}],
489491
}
490492
}
493+
return config
494+
495+
496+
def _codex_hooks_config(args: argparse.Namespace) -> dict:
497+
cmd_prefix = _hook_command_prefix(args, provider="codex")
498+
return {
499+
"hooks": {
500+
"SessionStart": [{
501+
"matcher": "startup|resume|clear|compact",
502+
"hooks": [{
503+
"type": "command",
504+
"command": f"{cmd_prefix} session-start",
505+
}],
506+
}],
507+
"UserPromptSubmit": [{
508+
"hooks": [{
509+
"type": "command",
510+
"command": f"{cmd_prefix} user-prompt",
511+
}],
512+
}],
513+
"PreToolUse": [{
514+
"matcher": ".*",
515+
"hooks": [{
516+
"type": "command",
517+
"command": f"{cmd_prefix} pre-tool",
518+
}],
519+
}],
520+
"PostToolUse": [{
521+
"matcher": ".*",
522+
"hooks": [{
523+
"type": "command",
524+
"command": f"{cmd_prefix} post-tool",
525+
}],
526+
}],
527+
"Stop": [{
528+
"hooks": [{
529+
"type": "command",
530+
"command": f"{cmd_prefix} stop",
531+
}],
532+
}],
533+
}
534+
}
535+
536+
537+
def cmd_setup(args: argparse.Namespace) -> None:
538+
"""Generate hooks configuration for supported agent CLIs."""
539+
cli = getattr(args, "cli", "claude") or "claude"
491540

492-
output = json.dumps(config, indent=2)
541+
configs: list[tuple[str, str, dict]] = []
542+
if cli in ("claude", "all"):
543+
configs.append(("Claude Code", "~/.claude/settings.json", _claude_hooks_config(args)))
544+
if cli in ("codex", "all"):
545+
configs.append(("OpenAI Codex", "~/.codex/hooks.json", _codex_hooks_config(args)))
493546

494-
sys.stderr.write("Add this to ~/.claude/settings.json:\n\n")
547+
for idx, (name, path, config) in enumerate(configs):
548+
if idx:
549+
sys.stdout.write("\n")
550+
sys.stderr.write(f"Add this to {path} for {name}:\n\n")
551+
sys.stdout.write(json.dumps(config, indent=2) + "\n")
495552

496-
sys.stdout.write(output + "\n")
497553
sys.stderr.write(
498-
"\nThis captures the full Claude Code session: user prompts, "
499-
"assistant responses, and every tool call (Bash, Edit, Write, "
500-
"Read, Agent, and all MCP tools).\n"
554+
"\nThis captures full agent sessions: user prompts, assistant "
555+
"responses, and hook-visible tool calls.\n"
501556
"Replay with: agent-strace replay\n"
502557
)
503558

@@ -622,12 +677,14 @@ def build_parser() -> argparse.ArgumentParser:
622677
p_stats.add_argument("--include-subagents", action="store_true",
623678
help="roll up stats across all subagent sessions")
624679

625-
# hook (called by Claude Code hooks system)
626-
p_hook = sub.add_parser("hook", help="handle a Claude Code hook event (internal)")
680+
# hook (called by agent CLI hooks systems)
681+
p_hook = sub.add_parser("hook", help="handle an agent CLI hook event (internal)")
682+
p_hook.add_argument("--provider", choices=["claude", "codex"], default="claude",
683+
help="hook provider (default: claude)")
627684
p_hook.add_argument("event", nargs="?", help="hook event: session-start, session-end, pre-tool, post-tool, post-tool-failure")
628685

629-
# setup (generate Claude Code hooks config)
630-
p_setup = sub.add_parser("setup", help="generate Claude Code hooks configuration")
686+
# setup (generate agent CLI hooks config)
687+
p_setup = sub.add_parser("setup", help="generate agent CLI hooks configuration")
631688
setup_redaction = p_setup.add_mutually_exclusive_group()
632689
setup_redaction.add_argument(
633690
"--redact",
@@ -640,6 +697,8 @@ def build_parser() -> argparse.ArgumentParser:
640697
help="disable automatic secret redaction in generated hooks",
641698
)
642699
p_setup.add_argument("--global", dest="global_config", action="store_true", help="output config for ~/.claude/settings.json (all projects)")
700+
p_setup.add_argument("--cli", choices=["claude", "codex", "all"], default="claude",
701+
help="agent CLI to configure (default: claude)")
643702

644703
# import (Claude Code JSONL session logs)
645704
p_import = sub.add_parser("import", help="import a Claude Code JSONL session log")
@@ -1332,7 +1391,10 @@ def main() -> None:
13321391

13331392
# hook subcommand is handled separately (reads stdin)
13341393
if args.command == "hook":
1335-
hook_main([args.event] if args.event else [])
1394+
hook_args = ["--provider", getattr(args, "provider", "claude")]
1395+
if args.event:
1396+
hook_args.append(args.event)
1397+
hook_main(hook_args)
13361398
sys.exit(0)
13371399

13381400
if args.command == "setup":

0 commit comments

Comments
 (0)