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
7 changes: 5 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,13 +110,15 @@ rtk init --agent windsurf # Windsurf
rtk init --agent cline # Cline / Roo Code
rtk init --agent kilocode # Kilo Code
rtk init --agent antigravity # Google Antigravity
rtk init --agent codebuddy # CodeBuddy Code CLI
rtk init -g --agent codebuddy # CodeBuddy Code CLI + App/IDE plugin
rtk init --agent hermes # Hermes

# 2. Restart your AI tool, then test
git status # Automatically rewritten to rtk git status
```

Hook-based agents rewrite Bash commands (e.g., `git status` -> `rtk git status`) before execution. Plugin-based agents, including Hermes, use their plugin API to rewrite commands before execution. The agent receives compact output without needing to call `rtk` explicitly.
Hook-based agents rewrite Bash commands (e.g., `git status` -> `rtk git status`) before execution. Plugin-based agents, including Hermes and CodeBuddy App/IDE, use their plugin API to rewrite commands before execution. The agent receives compact output without needing to call `rtk` explicitly.

**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 @@ -351,7 +353,7 @@ rtk git status

## Supported AI Tools

RTK supports 13 AI coding tools. Each integration rewrites shell commands to `rtk` equivalents for 60-90% token savings where the agent supports command interception.
RTK supports multiple AI coding tools. Each integration rewrites shell commands to `rtk` equivalents for 60-90% token savings where the agent supports command interception.

| Tool | Install | Method |
|------|---------|--------|
Expand All @@ -369,6 +371,7 @@ RTK supports 13 AI coding tools. Each integration rewrites shell commands to `rt
| **Mistral Vibe** | Planned ([#800](https://github.com/rtk-ai/rtk/issues/800)) | Blocked on upstream |
| **Kilo Code** | `rtk init --agent kilocode` | .kilocode/rules/rtk-rules.md (project-scoped) |
| **Google Antigravity** | `rtk init --agent antigravity` | .agents/rules/antigravity-rtk-rules.md (project-scoped) |
| **CodeBuddy Code** | `rtk init --agent codebuddy` / `rtk init -g --agent codebuddy` | PreToolUse hook; global install also enables App/IDE marketplace plugin |

For per-agent setup details, override controls, and graceful degradation, see the [Supported Agents guide](https://www.rtk-ai.app/guide/getting-started/supported-agents). The Hermes plugin source and tests live in `hooks/hermes/`; installed Hermes runtime files still live under `~/.hermes/plugins/rtk-rewrite/`.

Expand Down
26 changes: 24 additions & 2 deletions docs/guide/getting-started/supported-agents.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
title: Supported Agents
description: How to integrate RTK with Claude Code, Cursor, Copilot, Cline, Windsurf, Codex, OpenCode, Hermes, Kilo Code, and Antigravity
description: How to integrate RTK with Claude Code, Cursor, Copilot, CodeBuddy Code, Cline, Windsurf, Codex, OpenCode, Hermes, Kilo Code, and Antigravity
sidebar:
order: 3
---
Expand Down Expand Up @@ -33,6 +33,7 @@ Agent runs "cargo test"
| GitHub Copilot CLI | Shell hook (deny-with-suggestion) | No (agent retries) |
| Cursor | Shell hook (`preToolUse`) | Yes |
| Gemini CLI | Rust binary (`BeforeTool`) | Yes |
| CodeBuddy Code | Shell hook (`PreToolUse`) + App/IDE marketplace plugin | Yes |
| OpenCode | TypeScript plugin (`tool.execute.before`) | Yes |
| OpenClaw | TypeScript plugin (`before_tool_call`) | Yes |
| Pi | TypeScript extension (`tool_call` event) | Yes |
Expand Down Expand Up @@ -78,6 +79,27 @@ rtk init --global --copilot
rtk init --global --gemini
```

### CodeBuddy Code

```bash
# Project-local CLI hook
rtk init --agent codebuddy

# Global CLI hook + App/IDE marketplace plugin
rtk init -g --agent codebuddy
```

The project-local install writes `.codebuddy/settings.json` with a `PreToolUse` hook that delegates Bash commands to `rtk hook codebuddy`. The global install writes `~/.codebuddy/settings.json`, installs the RTK plugin under `~/.codebuddy/plugins/marketplaces/codebuddy-plugins-official/plugins/rtk/`, and enables `rtk@codebuddy-plugins-official` in `enabledPlugins` so the CodeBuddy App/IDE path is active too.

Uninstall:

```bash
rtk init --uninstall --agent codebuddy
rtk init --uninstall -g --agent codebuddy
```

Uninstall removes the CodeBuddy hook entries, the global RTK plugin directory, and the plugin enable flag. It also cleans up legacy `rtk-tx` CodeBuddy hook and plugin entries when present.

### OpenCode

```bash
Expand Down Expand Up @@ -173,7 +195,7 @@ Support is blocked on upstream `BeforeToolCallback` ([mistral-vibe#531](https://
| **Plugin** | TypeScript, JavaScript, or Python in agent's plugin system | Transparent, in-place mutation when the agent allows it |
| **Rules file** | Prompt-level instructions | Guidance only — agent is told to prefer `rtk <cmd>` |

Rules file integrations (Cline, Windsurf, Codex, Kilo Code, Antigravity) rely on the model following instructions. Full hook integrations (Claude Code, Cursor, Gemini) are guaranteed the command is rewritten before the agent sees it. Plugin integrations (OpenCode, Pi) use in-place mutation via the agent's TypeScript extension API.
Rules file integrations (Cline, Windsurf, Codex, Kilo Code, Antigravity) rely on the model following instructions. Full hook integrations (Claude Code, Cursor, Gemini, CodeBuddy Code CLI) are guaranteed because the command is rewritten before the agent sees it. Plugin integrations (CodeBuddy App/IDE, OpenCode, Pi) use in-place mutation through the agent's plugin or extension API.

## Windows support

Expand Down
26 changes: 25 additions & 1 deletion hooks/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

**Deployed hook artifacts** — the actual files installed on user machines by `rtk init`. These are shell scripts, TypeScript plugins, and rules files that run outside the Rust binary. They are **thin delegates**: parse agent-specific JSON, call `rtk rewrite` as a subprocess, format agent-specific response. Zero filtering logic lives here.

Owns: per-agent hook scripts and configuration files for 9 supported agents (Claude Code, Copilot, Cursor, Cline, Windsurf, Codex, OpenCode, Hermes, Pi).
Owns: per-agent hook scripts and configuration files for supported agents (Claude Code, CodeBuddy Code, Copilot, Cursor, Cline, Windsurf, Codex, OpenCode, Hermes, Pi).

Does **not** own: hook installation/uninstallation (that's `src/hooks/init.rs`), the rewrite pattern registry (that's `discover/registry`), or integrity verification (that's `src/hooks/integrity.rs`).

Expand Down Expand Up @@ -48,6 +48,7 @@ Each agent subdirectory has its own README with hook-specific details:
| Agent | Mechanism | Hook Type | Can Modify Command? |
|-------|-----------|-----------|---------------------|
| Claude Code | Shell hook (`PreToolUse`) | Transparent rewrite | Yes (`updatedInput`) |
| CodeBuddy Code | Rust binary (`rtk hook codebuddy`) | Transparent rewrite | Yes (`updatedInput`, `modifiedInput`) |
| VS Code Copilot Chat | Rust binary (`rtk hook copilot`) | Transparent rewrite | Yes (`updatedInput`) |
| GitHub Copilot CLI | Rust binary (`rtk hook copilot`) | Deny-with-suggestion | No (agent retries) |
| Cursor | Shell hook (`preToolUse`) | Transparent rewrite | Yes (`updated_input`) |
Expand Down Expand Up @@ -85,6 +86,29 @@ Each agent subdirectory has its own README with hook-specific details:
}
```

### CodeBuddy Code (Rust Binary)

CodeBuddy uses a Claude-compatible `PreToolUse` payload and RTK responds with both `updatedInput` and `modifiedInput` for compatibility with CodeBuddy's CLI and App/IDE plugin paths. Project installs write `.codebuddy/settings.json`; global installs also write and enable the CodeBuddy marketplace plugin under `~/.codebuddy/plugins/marketplaces/codebuddy-plugins-official/plugins/rtk/`.

**Command**:

```bash
rtk hook codebuddy
```

**Output** (stdout, when rewritten):

```json
{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecisionReason": "RTK auto-rewrite",
"updatedInput": { "command": "rtk git status" },
"modifiedInput": { "command": "rtk git status" }
}
}
```

### Cursor (Shell Hook)

**Input**: Same as Claude Code.
Expand Down
5 changes: 4 additions & 1 deletion src/hooks/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

The **lifecycle management** layer for LLM agent hooks: install, uninstall, verify integrity, audit usage, and manage trust. This component creates and maintains the hook artifacts that live in `hooks/` (root), but does **not** execute rewrite logic itself — that lives in `discover/registry`.

Owns: `rtk init` installation flows (5 agents via `AgentTarget` enum + 3 special modes: Gemini, Codex, OpenCode), SHA-256 integrity verification, hook version checking, audit log analysis, `rtk rewrite` CLI entry point, and TOML filter trust management.
Owns: `rtk init` installation flows (6 agents via `AgentTarget` enum + 3 special modes: Gemini, Codex, OpenCode), SHA-256 integrity verification, hook version checking, audit log analysis, `rtk rewrite` CLI entry point, and TOML filter trust management.

Does **not** own: the deployed hook scripts themselves (that's `hooks/`), the rewrite pattern registry (that's `discover/`), or command filtering (that's `cmds/`).

Expand All @@ -31,6 +31,8 @@ LLM agent integration layer that installs, validates, and executes command-rewri
| Codex | `rtk init --codex` | RTK.md in `$CODEX_HOME` or `~/.codex` | AGENTS.md |
| Cursor | `rtk init -g --agent cursor` | Cursor hook | hooks.json |
| Pi | `rtk init --agent pi` | `.pi/extensions/rtk.ts` | -- |
| CodeBuddy project | `rtk init --agent codebuddy` | `.codebuddy/settings.json` | `hooks.PreToolUse` |
| CodeBuddy global | `rtk init -g --agent codebuddy` | `~/.codebuddy/settings.json`, marketplace plugin | `hooks.PreToolUse`, `enabledPlugins` |
| Hermes | `rtk init --agent hermes` | Python plugin in `~/.hermes/plugins/rtk-rewrite/` | `config.yaml` `plugins.enabled` |


Expand Down Expand Up @@ -89,6 +91,7 @@ Rules are loaded from all Claude Code `settings.json` files (project + global, i
| Gemini CLI (rtk hook gemini) | No (allow/deny only) | allow (limitation — no ask mode in Gemini) |
| Copilot CLI (rtk hook copilot) | No updatedInput | deny-with-suggestion (unchanged) |
| Codex | ask parsed but no-op | allow (limitation — fails open) |
| CodeBuddy Code (rtk hook codebuddy) | Host-managed hook approval | rewrite with `updatedInput` + `modifiedInput`; Claude permission files are not consulted |

### Implementation

Expand Down
8 changes: 8 additions & 0 deletions src/hooks/constants.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ pub const BEFORE_TOOL_KEY: &str = "BeforeTool";
pub const CLAUDE_HOOK_COMMAND: &str = "rtk hook claude";
/// Native Rust hook command for Cursor (replaces rtk-rewrite.sh).
pub const CURSOR_HOOK_COMMAND: &str = "rtk hook cursor";
/// Native Rust hook command for CodeBuddy Code.
pub const CODEBUDDY_HOOK_COMMAND: &str = "rtk hook codebuddy";

pub const CONFIG_DIR: &str = ".config";
pub const OPENCODE_SUBDIR: &str = "opencode";
Expand All @@ -21,6 +23,12 @@ pub const OPENCODE_PLUGIN_FILE: &str = "rtk.ts";
pub const CURSOR_DIR: &str = ".cursor";
pub const CODEX_DIR: &str = ".codex";
pub const GEMINI_DIR: &str = ".gemini";
pub const CODEBUDDY_DIR: &str = ".codebuddy";
pub const CODEBUDDY_PLUGIN_NAME: &str = "rtk";
pub const CODEBUDDY_PLUGIN_MARKETPLACE: &str = "codebuddy-plugins-official";
pub const CODEBUDDY_PLUGIN_ENABLED_KEY: &str = "rtk@codebuddy-plugins-official";
pub const CODEBUDDY_PLUGIN_MANIFEST_DIR: &str = ".codebuddy-plugin";
pub const CODEBUDDY_PLUGIN_MANIFEST_FILE: &str = "plugin.json";

pub const PI_DIR: &str = ".pi/agent";
pub const PI_LOCAL_DIR: &str = ".pi";
Expand Down
150 changes: 144 additions & 6 deletions src/hooks/hook_cmd.rs
Original file line number Diff line number Diff line change
Expand Up @@ -297,7 +297,17 @@ enum PayloadAction {
Ignore,
}

fn process_claude_payload(v: &Value) -> PayloadAction {
#[derive(Clone, Copy)]
enum PermissionMode {
Claude,
NoPermissionCheck,
}

fn process_claude_payload(
v: &Value,
permission_mode: PermissionMode,
include_modified_input: bool,
) -> PayloadAction {
let cmd = match v
.pointer("/tool_input/command")
.and_then(|c| c.as_str())
Expand All @@ -307,7 +317,10 @@ fn process_claude_payload(v: &Value) -> PayloadAction {
None => return PayloadAction::Ignore,
};

let verdict = permissions::check_command(cmd);
let verdict = match permission_mode {
PermissionMode::Claude => permissions::check_command(cmd),
PermissionMode::NoPermissionCheck => PermissionVerdict::Default,
};
if verdict == PermissionVerdict::Deny {
return PayloadAction::Skip {
reason: "skip:deny_rule",
Expand Down Expand Up @@ -336,9 +349,16 @@ fn process_claude_payload(v: &Value) -> PayloadAction {
let mut hook_output = json!({
"hookEventName": PRE_TOOL_USE_KEY,
"permissionDecisionReason": "RTK auto-rewrite",
"updatedInput": updated_input
"updatedInput": updated_input.clone()
});

if include_modified_input {
hook_output
.as_object_mut()
.unwrap()
.insert("modifiedInput".into(), updated_input);
}

if verdict == PermissionVerdict::Allow {
hook_output
.as_object_mut()
Expand All @@ -355,6 +375,19 @@ fn process_claude_payload(v: &Value) -> PayloadAction {

/// Run the Claude Code PreToolUse hook natively.
pub fn run_claude() -> Result<()> {
run_claude_compatible_hook("claude", PermissionMode::Claude, false)
}

/// Run the CodeBuddy Code PreToolUse hook natively.
pub fn run_codebuddy() -> Result<()> {
run_claude_compatible_hook("codebuddy", PermissionMode::NoPermissionCheck, true)
}

fn run_claude_compatible_hook(
adapter: &str,
permission_mode: PermissionMode,
include_modified_input: bool,
) -> Result<()> {
let input = read_stdin_limited()?;

let input = input.trim();
Expand All @@ -365,12 +398,15 @@ pub fn run_claude() -> Result<()> {
let v: Value = match serde_json::from_str(input) {
Ok(v) => v,
Err(e) => {
let _ = writeln!(io::stderr(), "[rtk hook] Failed to parse JSON input: {e}");
let _ = writeln!(
io::stderr(),
"[rtk hook {adapter}] Failed to parse JSON input: {e}"
);
return Ok(());
}
};

match process_claude_payload(&v) {
match process_claude_payload(&v, permission_mode, include_modified_input) {
PayloadAction::Rewrite {
cmd,
rewritten,
Expand All @@ -391,7 +427,16 @@ pub fn run_claude() -> Result<()> {
#[cfg(test)]
fn run_claude_inner(input: &str) -> Option<String> {
let v: Value = serde_json::from_str(input).ok()?;
match process_claude_payload(&v) {
match process_claude_payload(&v, PermissionMode::Claude, false) {
PayloadAction::Rewrite { output, .. } => Some(output.to_string()),
_ => None,
}
}

#[cfg(test)]
fn run_codebuddy_inner(input: &str) -> Option<String> {
let v: Value = serde_json::from_str(input).ok()?;
match process_claude_payload(&v, PermissionMode::NoPermissionCheck, true) {
PayloadAction::Rewrite { output, .. } => Some(output.to_string()),
_ => None,
}
Expand Down Expand Up @@ -780,6 +825,99 @@ mod tests {
assert!(run_claude_inner(&input).is_none());
}

// --- CodeBuddy handler ---

fn codebuddy_input(cmd: &str) -> String {
json!({
"hook_event_name": "PreToolUse",
"tool_name": "Bash",
"tool_input": { "command": cmd }
})
.to_string()
}

fn codebuddy_input_with_fields(cmd: &str, timeout: u64, description: &str) -> String {
json!({
"hook_event_name": "PreToolUse",
"tool_name": "Bash",
"tool_input": {
"command": cmd,
"timeout": timeout,
"description": description,
"extra": { "keep": true }
}
})
.to_string()
}

#[test]
fn test_codebuddy_rewrite_git_status() {
let result = run_codebuddy_inner(&codebuddy_input("git status")).unwrap();
let v: Value = serde_json::from_str(&result).unwrap();
let cmd = v
.pointer("/hookSpecificOutput/updatedInput/command")
.and_then(|c| c.as_str())
.unwrap();
assert_eq!(cmd, "rtk git status");
}

#[test]
fn test_codebuddy_rewrite_preserves_tool_input_fields() {
let input = codebuddy_input_with_fields("git status", 30000, "Check repo status");
let result = run_codebuddy_inner(&input).unwrap();
let v: Value = serde_json::from_str(&result).unwrap();
let updated = &v["hookSpecificOutput"]["updatedInput"];
assert_eq!(updated["command"], "rtk git status");
assert_eq!(updated["timeout"], 30000);
assert_eq!(updated["description"], "Check repo status");
assert_eq!(updated["extra"]["keep"], true);
}

#[test]
fn test_codebuddy_modified_input_matches_updated_input() {
let result = run_codebuddy_inner(&codebuddy_input("git status")).unwrap();
let v: Value = serde_json::from_str(&result).unwrap();
let hook = &v["hookSpecificOutput"];
assert_eq!(hook["modifiedInput"], hook["updatedInput"]);
}

#[test]
fn test_claude_output_does_not_include_codebuddy_modified_input() {
let result = run_claude_inner(&claude_input("git status")).unwrap();
let v: Value = serde_json::from_str(&result).unwrap();
assert!(v["hookSpecificOutput"].get("modifiedInput").is_none());
}

#[test]
fn test_codebuddy_output_uses_host_managed_permissions() {
let result = run_codebuddy_inner(&codebuddy_input("git status")).unwrap();
let v: Value = serde_json::from_str(&result).unwrap();
let hook = &v["hookSpecificOutput"];
assert_eq!(hook["updatedInput"]["command"], "rtk git status");
assert!(hook.get("permissionDecision").is_none());
assert_eq!(hook["permissionDecisionReason"], "RTK auto-rewrite");
}

#[test]
fn test_codebuddy_malformed_json_passthrough() {
assert!(run_codebuddy_inner("not valid json {{{").is_none());
}

#[test]
fn test_codebuddy_unsupported_command_passthrough() {
assert!(run_codebuddy_inner(&codebuddy_input("htop")).is_none());
}

#[test]
fn test_codebuddy_heredoc_passthrough() {
assert!(run_codebuddy_inner(&codebuddy_input("cat <<EOF\nhello\nEOF")).is_none());
}

#[test]
fn test_codebuddy_already_rtk_passthrough() {
assert!(run_codebuddy_inner(&codebuddy_input("rtk git status")).is_none());
}

// --- Cursor handler ---

fn cursor_input(cmd: &str) -> String {
Expand Down
Loading
Loading