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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Features

* **codex:** add native Codex hook integration with `PreToolUse` deny-and-retry, `CODEX_HOME` support, and Windows prompt-only fallback

### Bug Fixes

* **diff:** correct truncation overflow count in condense_unified_diff ([#833](https://github.com/rtk-ai/rtk/pull/833)) ([5399f83](https://github.com/rtk-ai/rtk/commit/5399f83))
Expand Down
23 changes: 18 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,10 +108,10 @@ rtk init --agent windsurf # Windsurf
rtk init --agent cline # Cline / Roo Code

# 2. Restart your AI tool, then test
git status # Automatically rewritten to rtk git status
git status # Hook rewrites it or suggests `rtk git status`, depending on the tool
```

The hook transparently rewrites Bash commands (e.g., `git status` -> `rtk git status`) before execution. Claude never sees the rewrite, it just gets compressed output.
Hooks either transparently rewrite Bash commands (for example `git status` -> `rtk git status`) or, when a harness cannot update the command input yet, block the raw command and tell the model to retry with the exact `rtk ...` replacement.

**Important:** the hook only runs on Bash tool calls. Claude Code built-in tools like `Read`, `Grep`, and `Glob` do not pass through the Bash hook, so they are not auto-rewritten. To get RTK's compact output for those workflows, use shell commands (`cat`/`head`/`tail`, `rg`/`grep`, `find`) or call `rtk read`, `rtk grep`, or `rtk find` directly.

Expand Down Expand Up @@ -305,13 +305,15 @@ RTK supports 10 AI coding tools. Each integration transparently rewrites shell c
| **GitHub Copilot CLI** | `rtk init -g --copilot` | PreToolUse deny-with-suggestion (CLI limitation) |
| **Cursor** | `rtk init -g --agent cursor` | preToolUse hook (hooks.json) |
| **Gemini CLI** | `rtk init -g --gemini` | BeforeTool hook (`rtk hook gemini`) |
| **Codex** | `rtk init -g --codex` | AGENTS.md + RTK.md instructions |
| **Codex** | `rtk init -g --codex` | PreToolUse deny-with-suggestion (`.codex/hooks.json` + `.codex/config.toml`) |
| **Windsurf** | `rtk init --agent windsurf` | .windsurfrules (project-scoped) |
| **Cline / Roo Code** | `rtk init --agent cline` | .clinerules (project-scoped) |
| **OpenCode** | `rtk init -g --opencode` | Plugin TS (tool.execute.before) |
| **OpenClaw** | `openclaw plugins install ./openclaw` | Plugin TS (before_tool_call) |
| **Mistral Vibe** | Planned (#800) | Blocked on upstream BeforeToolCallback |

Codex on Windows currently falls back to prompt-only setup because upstream Codex does not run lifecycle hooks there.

### Claude Code (default)

```bash
Expand Down Expand Up @@ -354,9 +356,20 @@ Creates `~/.gemini/hooks/rtk-hook-gemini.sh` + patches `~/.gemini/settings.json`

```bash
rtk init -g --codex
rtk init --codex
rtk init -g --codex --uninstall
```

Creates `~/.codex/RTK.md` + `~/.codex/AGENTS.md` with `@RTK.md` reference. Codex reads these as global instructions.
On macOS and Linux, global install writes `${CODEX_HOME:-~/.codex}/RTK.md`, `${CODEX_HOME:-~/.codex}/AGENTS.md`, `${CODEX_HOME:-~/.codex}/config.toml`, and `${CODEX_HOME:-~/.codex}/hooks.json`. Project-scoped install writes the same files under `./.codex/`.

RTK enables `features.codex_hooks = true` and installs a `PreToolUse` Bash hook that runs `rtk hook codex`.

Codex does not support transparent `updatedInput` rewrites yet, so supported raw Bash commands are denied with the exact `rtk ...` replacement instead of being silently rewritten.

Notes:
- If `CODEX_HOME` is set, `rtk init -g --codex` uses that directory instead of `~/.codex`.
- On Windows, RTK falls back to `RTK.md` + `AGENTS.md` instructions only because Codex lifecycle hooks are currently disabled upstream.
- Project-scoped `.codex/` installs only activate when Codex trusts the project.

### Windsurf

Expand Down Expand Up @@ -457,7 +470,7 @@ FAILED: 2/15 tests
### Uninstall

```bash
rtk init -g --uninstall # Remove hook, RTK.md, settings.json entry
rtk init -g --uninstall # Remove RTK-managed hook, RTK.md, and harness config entry
cargo uninstall rtk # Remove binary
brew uninstall rtk # If installed via Homebrew
```
Expand Down
15 changes: 12 additions & 3 deletions hooks/codex/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,15 @@
## Specifics

- Prompt-level guidance via awareness document -- no programmatic hook
- `rtk-awareness.md` is injected into `AGENTS.md` with an `@RTK.md` reference
- Installed to `~/.codex/` by `rtk init --codex`
- Awareness document is injected into `AGENTS.md` with an `@RTK.md` reference
- On macOS and Linux, `rtk init --codex` also installs `config.toml` and `hooks.json` so Codex can run `rtk hook codex` for `PreToolUse`
- Codex currently uses deny-and-retry rather than transparent rewrite because `updatedInput` is not supported yet
- On Windows, RTK falls back to prompt-only guidance because Codex lifecycle hooks are disabled upstream
- Global install goes to `${CODEX_HOME:-~/.codex}`
- Project installs only activate for trusted projects

## Test

```bash
bash hooks/codex/test-rtk-rewrite.sh
```
4 changes: 4 additions & 0 deletions hooks/codex/rtk-awareness.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@

Always prefix shell commands with `rtk`.

If Codex hooks are enabled, raw supported Bash commands may be blocked with a message telling you to rerun the `rtk ...` form. Follow that guidance rather than retrying the raw command.

On Windows, or when Codex does not load project hooks such as untrusted local `.codex` config, this prompt guidance may be the only active RTK layer.

Examples:

```bash
Expand Down
148 changes: 148 additions & 0 deletions hooks/codex/test-rtk-rewrite.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
#!/usr/bin/env bash
# Test suite for rtk hook codex.
# Feeds mock Codex PreToolUse JSON through `rtk hook codex` and verifies deny/pass-through behavior.
#
# Usage: bash hooks/codex/test-rtk-rewrite.sh

RTK="${RTK:-rtk}"
PASS=0
FAIL=0
TOTAL=0

GREEN='\033[32m'
RED='\033[31m'
DIM='\033[2m'
RESET='\033[0m'

codex_bash_input() {
local cmd="$1"
jq -cn --arg cmd "$cmd" '{"tool_name":"Bash","tool_input":{"command":$cmd}}'
}

non_bash_input() {
jq -cn '{"tool_name":"Edit","tool_input":{"command":"git status"}}'
}

test_deny() {
local description="$1"
local input_cmd="$2"
local expected_rtk="$3"
TOTAL=$((TOTAL + 1))

local output
output=$(codex_bash_input "$input_cmd" | "$RTK" hook codex 2>/dev/null) || true

local decision reason
decision=$(echo "$output" | jq -r '.hookSpecificOutput.permissionDecision // empty' 2>/dev/null)
reason=$(echo "$output" | jq -r '.hookSpecificOutput.permissionDecisionReason // empty' 2>/dev/null)

if [ "$decision" = "deny" ] && echo "$reason" | grep -qF "$expected_rtk"; then
printf " ${GREEN}DENY${RESET} %s ${DIM}→ %s${RESET}\n" "$description" "$expected_rtk"
PASS=$((PASS + 1))
else
printf " ${RED}FAIL${RESET} %s\n" "$description"
printf " expected decision: deny, reason containing: %s\n" "$expected_rtk"
printf " actual decision: %s\n" "$decision"
printf " actual reason: %s\n" "$reason"
FAIL=$((FAIL + 1))
fi
}

test_allow() {
local description="$1"
local input="$2"
TOTAL=$((TOTAL + 1))

local output
output=$(echo "$input" | "$RTK" hook codex 2>/dev/null) || true

if [ -z "$output" ]; then
printf " ${GREEN}PASS${RESET} %s ${DIM}→ (no output)${RESET}\n" "$description"
PASS=$((PASS + 1))
else
printf " ${RED}FAIL${RESET} %s\n" "$description"
printf " expected: (no output)\n"
printf " actual: %s\n" "$output"
FAIL=$((FAIL + 1))
fi
}

echo "============================================"
echo " RTK Codex Hook Test Suite"
echo "============================================"
echo ""

echo "--- Deny with RTK suggestion ---"

test_deny "git status" \
"git status" \
"rtk git status"

test_deny "cargo test" \
"cargo test" \
"rtk cargo test"

test_deny "gh pr list" \
"gh pr list" \
"rtk gh"

echo ""
echo "--- Pass-through ---"

test_allow "already rtk" \
"$(codex_bash_input "rtk git status")"

test_allow "heredoc" \
"$(codex_bash_input "cat <<'EOF'
hello
EOF")"

test_allow "unknown command" \
"$(codex_bash_input "htop")"

test_allow "non-bash tool" \
"$(non_bash_input)"

echo ""
echo "--- Output format ---"

TOTAL=$((TOTAL + 1))
raw_output=$(codex_bash_input "git status" | "$RTK" hook codex 2>/dev/null)
if echo "$raw_output" | jq . >/dev/null 2>&1; then
printf " ${GREEN}PASS${RESET} Codex: output is valid JSON\n"
PASS=$((PASS + 1))
else
printf " ${RED}FAIL${RESET} Codex: output is not valid JSON: %s\n" "$raw_output"
FAIL=$((FAIL + 1))
fi

TOTAL=$((TOTAL + 1))
decision=$(echo "$raw_output" | jq -r '.hookSpecificOutput.permissionDecision')
if [ "$decision" = "deny" ]; then
printf " ${GREEN}PASS${RESET} Codex: hookSpecificOutput.permissionDecision == \"deny\"\n"
PASS=$((PASS + 1))
else
printf " ${RED}FAIL${RESET} Codex: expected \"deny\", got \"%s\"\n" "$decision"
FAIL=$((FAIL + 1))
fi

TOTAL=$((TOTAL + 1))
reason=$(echo "$raw_output" | jq -r '.hookSpecificOutput.permissionDecisionReason')
if echo "$reason" | grep -qE '`rtk [^`]+`'; then
printf " ${GREEN}PASS${RESET} Codex: reason contains backtick-quoted rtk command ${DIM}→ %s${RESET}\n" "$reason"
PASS=$((PASS + 1))
else
printf " ${RED}FAIL${RESET} Codex: reason missing backtick-quoted command: %s\n" "$reason"
FAIL=$((FAIL + 1))
fi

echo ""
echo "============================================"
if [ $FAIL -eq 0 ]; then
printf " ${GREEN}ALL $TOTAL TESTS PASSED${RESET}\n"
else
printf " ${RED}$FAIL FAILED${RESET} / $TOTAL total ($PASS passed)\n"
fi
echo "============================================"

exit $FAIL
Loading