Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 10 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1465,7 +1465,7 @@ If tokf rewrite fails or no filter matches, the command passes through unmodifie

## OpenAI Codex CLI

tokf integrates with [OpenAI Codex CLI](https://github.com/openai/codex) via a skill that instructs the agent to prefix supported commands with `tokf run`.
tokf integrates with [OpenAI Codex CLI](https://github.com/openai/codex) via a Codex `PreToolUse` hook plus skills for guidance and discovery.

**Install (project-local):**
```sh
Expand All @@ -1477,7 +1477,13 @@ tokf hook install --tool codex
tokf hook install --tool codex --global
```

This writes `.agents/skills/tokf-run/SKILL.md` (or `~/.agents/skills/tokf-run/SKILL.md` for `--global`), which Codex auto-discovers. Unlike the Claude Code hook (which intercepts commands at the tool level), the Codex integration is skill-based: it teaches the agent to use `tokf run` as a command prefix. If tokf is not installed, the agent falls back to running commands without the prefix (fail-safe).
This writes a Codex `PreToolUse` hook to `.codex/hooks.json` (or `~/.codex/hooks.json` for `--global`) and installs `.agents/skills/tokf-run/SKILL.md` (or `~/.agents/skills/tokf-run/SKILL.md` for `--global`), which Codex auto-discovers.

Codex CLI 0.124.0 and newer enable lifecycle hooks by default. tokf's Codex integration uses `PreToolUse`, which first appeared in Codex 0.117.0.

If the installed hook does not run, upgrade Codex or check that hooks are enabled, then restart Codex. Codex 0.129.0+ prefers `[features].hooks = true`; older builds used the legacy alias `[features].codex_hooks = true`.

Codex CLI 0.131.0 and newer support `PreToolUse` `updatedInput`, so tokf transparently rewrites matching Bash commands in-place. During installation, tokf checks the local `codex --version` output and installs a conservative deny-and-rerun fallback for older or unknown Codex versions so the original command does not fail open. After upgrading Codex, rerun `tokf hook install --tool codex` so tokf can refresh the generated shim mode. Commands without a matching tokf filter pass through unchanged.

## Permission engines

Expand All @@ -1487,12 +1493,13 @@ tokf supports pluggable permission engines that analyse commands and decide whet

### Hook handle command

All hook-based integrations (Claude Code, Gemini CLI, Cursor) call `tokf hook handle` internally. The `--format` flag tells tokf which response protocol to use:
All hook-based integrations (Claude Code, Gemini CLI, Cursor, Codex) call `tokf hook handle` internally. The `--format` flag tells tokf which response protocol to use:

```sh
tokf hook handle # default: claude-code
tokf hook handle --format gemini # Gemini CLI protocol
tokf hook handle --format cursor # Cursor protocol
tokf hook handle --format codex # Codex CLI protocol
```

The hook scripts generated by `tokf hook install` set `--format` automatically — you don't need to pass it manually. The format also determines which JSON field the external permission engine should set (see [hook JSON reference](rewrites-config.md#hook-json-reference-for-engine-developers)).
Expand Down
8 changes: 5 additions & 3 deletions crates/tokf-cli/skills/codex-run/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ tokf compresses verbose command output so you receive concise, structured result

## Rule

When running a supported command, prefix it with `tokf run`:
When the tokf Codex hook is installed, run supported commands normally. The hook rewrites matching commands to `tokf run <command>` before execution.

If the hook is unavailable, blocked, or not rewriting a supported command, use the manual fallback:

```
tokf run <command>
Expand All @@ -26,7 +28,7 @@ tokf run docker ps

## Supported commands

The following commands have built-in filters. Prefix these with `tokf run`:
The following commands have built-in filters. The hook can rewrite them automatically; use `tokf run` manually only as fallback:

- `git status`, `git diff`, `git log`, `git push`, `git add`, `git commit`, `git show`
- `cargo build`, `cargo test`, `cargo check`, `cargo clippy`, `cargo install`
Expand All @@ -48,7 +50,7 @@ Commands not in this list pass through unchanged when prefixed with `tokf run`.
## Important rules

1. **Never double-prefix.** If a command already starts with `tokf run`, do not add it again.
2. **Arguments pass through.** Include all flags and arguments after the base command: `tokf run cargo test --release -- my_test`.
2. **Arguments pass through.** In manual fallback mode, include all flags and arguments after the base command: `tokf run cargo test --release -- my_test`.
3. **Fail-safe.** If `tokf` is not installed or not on PATH, run the command without the prefix.
4. **Environment variables.** Place env vars before `tokf run`: `RUST_LOG=debug tokf run cargo test`.
5. **Pipes.** Do not add redundant filtering pipes (e.g. `| grep`, `| tail`, `| head`) after `tokf run` commands — tokf already compresses the output. Piping tokf's output for other purposes (e.g. `tokf run cargo test | wc -l`) is fine.
48 changes: 37 additions & 11 deletions crates/tokf-cli/src/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ pub enum HookFormat {
Gemini,
#[value(name = "cursor")]
Cursor,
#[value(name = "codex")]
Codex,
}

/// CLI surface for `tokf doctor --sort`. Mirrors `tokf::doctor::SortBy`
Expand Down Expand Up @@ -595,13 +597,14 @@ pub fn cmd_skill_install(global: bool) -> i32 {
}
}

pub fn cmd_hook_handle(format: &HookFormat) -> i32 {
pub fn cmd_hook_handle(format: &HookFormat, no_cache: bool) -> i32 {
let outcome = match format {
HookFormat::ClaudeCode => hook::handle(),
HookFormat::Gemini => hook::handle_gemini(),
HookFormat::Cursor => hook::handle_cursor(),
HookFormat::ClaudeCode => hook::handle(no_cache),
HookFormat::Gemini => hook::handle_gemini(no_cache),
HookFormat::Cursor => hook::handle_cursor(no_cache),
HookFormat::Codex => hook::handle_codex(no_cache),
};
hook_outcome_exit_code(outcome)
hook_outcome_exit_code(format, outcome)
}

/// Map a `HookOutcome` to the process exit code expected by the host AI tool.
Expand All @@ -610,8 +613,11 @@ pub fn cmd_hook_handle(format: &HookFormat) -> i32 {
/// from stdout and shows the user the native prompt. Exit 2 would short-circuit
/// that path and trigger an unconditional block. `Deny` keeps exit 2 because
/// the JSON is already paired with a hard block in every supported host.
const fn hook_outcome_exit_code(outcome: hook::HookOutcome) -> i32 {
const fn hook_outcome_exit_code(format: &HookFormat, outcome: hook::HookOutcome) -> i32 {
match outcome {
// Codex parses blocking JSON only from successful hook exits; exit 2
// requires stderr text and would discard the structured deny reason.
hook::HookOutcome::Deny if matches!(format, HookFormat::Codex) => 0,
hook::HookOutcome::Deny => 2,
hook::HookOutcome::Ask | hook::HookOutcome::Allow | hook::HookOutcome::PassThrough => 0,
}
Expand All @@ -627,7 +633,7 @@ pub fn cmd_hook_install(
let result = match tool {
HookTool::ClaudeCode => hook::install(global, &tokf_bin, install_context),
HookTool::OpenCode => hook::opencode::install(global, &tokf_bin),
HookTool::Codex => hook::codex::install(global),
HookTool::Codex => hook::codex::install(global, &tokf_bin, install_context),
HookTool::GeminiCli => hook::gemini::install(global, &tokf_bin, install_context),
HookTool::Cursor => hook::cursor::install(global, &tokf_bin, install_context),
HookTool::Cline => hook::cline::install(global),
Expand All @@ -652,21 +658,41 @@ mod tests {
fn ask_outcome_exits_zero_so_claude_reads_json_decision() {
// Regression for #343: exit 2 short-circuits the JSON path and turns
// an "ask" verdict into an unconditional block.
assert_eq!(hook_outcome_exit_code(hook::HookOutcome::Ask), 0);
assert_eq!(
hook_outcome_exit_code(&HookFormat::ClaudeCode, hook::HookOutcome::Ask),
0
);
}

#[test]
fn allow_outcome_exits_zero() {
assert_eq!(hook_outcome_exit_code(hook::HookOutcome::Allow), 0);
assert_eq!(
hook_outcome_exit_code(&HookFormat::ClaudeCode, hook::HookOutcome::Allow),
0
);
}

#[test]
fn passthrough_outcome_exits_zero() {
assert_eq!(hook_outcome_exit_code(hook::HookOutcome::PassThrough), 0);
assert_eq!(
hook_outcome_exit_code(&HookFormat::ClaudeCode, hook::HookOutcome::PassThrough),
0
);
}

#[test]
fn deny_outcome_exits_two() {
assert_eq!(hook_outcome_exit_code(hook::HookOutcome::Deny), 2);
assert_eq!(
hook_outcome_exit_code(&HookFormat::ClaudeCode, hook::HookOutcome::Deny),
2
);
}

#[test]
fn codex_deny_outcome_exits_zero_so_codex_reads_json_block() {
assert_eq!(
hook_outcome_exit_code(&HookFormat::Codex, hook::HookOutcome::Deny),
0
);
}
}
Loading