Skip to content
Closed
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
35 changes: 35 additions & 0 deletions INSTALL.md
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,29 @@ rtk init -g --no-patch
# Restart Claude Code
```

### Kimi Code CLI User
```bash
# 1. Install RTK
cargo install --git https://github.com/rtk-ai/rtk
rtk gain # Verify

# 2. Setup for Kimi
rtk init -g --kimi
# → Answer 'y' when prompted to patch config.toml

# 3. Restart Kimi CLI
# 4. Test: git status (should use rtk)
```

### CI/CD or Automation (Kimi)
```bash
# Non-interactive setup
rtk init -g --kimi --auto-patch

# Verify in scripts
rtk init --show --kimi | grep "config.toml"
```

### Temporary Trial
```bash
# Install hook
Expand Down Expand Up @@ -255,6 +278,17 @@ rtk init -g --uninstall
# Restart Claude Code after uninstall
```

### Kimi Code CLI Uninstall
```bash
rtk init -g --kimi --uninstall

# What gets removed:
# - Hook entry from ~/.kimi/config.toml
# - Context: ~/.kimi/KIMI.md

# Restart Kimi CLI after uninstall
```

**For Local Projects**: Manually remove RTK block from `./CLAUDE.md`

### Binary Removal
Expand Down Expand Up @@ -391,6 +425,7 @@ Before each session:
- [ ] Verify RTK is installed: `rtk --version`
- [ ] If not installed → follow "Install from fork"
- [ ] If project not initialized → `rtk init`
- [ ] For Kimi users → `rtk init -g --kimi`
- [ ] Use `rtk` for ALL git/pnpm/test/vitest commands
- [ ] Check savings: `rtk gain`

Expand Down
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ rtk gain # Should show token savings stats
rtk init -g # Claude Code / Copilot (default)
rtk init -g --gemini # Gemini CLI
rtk init -g --codex # Codex (OpenAI)
rtk init -g --kimi # Kimi Code CLI
rtk init -g --agent cursor # Cursor
rtk init --agent windsurf # Windsurf
rtk init --agent cline # Cline / Roo Code
Expand Down Expand Up @@ -350,7 +351,7 @@ rtk git status

## Supported AI Tools

RTK supports 12 AI coding tools. Each integration transparently rewrites shell commands to `rtk` equivalents for 60-90% token savings.
RTK supports 13 AI coding tools. Each integration transparently rewrites shell commands to `rtk` equivalents for 60-90% token savings.

| Tool | Install | Method |
|------|---------|--------|
Expand All @@ -366,6 +367,7 @@ RTK supports 12 AI coding tools. Each integration transparently rewrites shell c
| **OpenClaw** | `openclaw plugins install ./openclaw` | Plugin TS (before_tool_call) |
| **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) |
| **Kimi Code CLI** | `rtk init -g --kimi` | PreToolUse hook (config.toml) |
| **Google Antigravity** | `rtk init --agent antigravity` | .agents/rules/antigravity-rtk-rules.md (project-scoped) |

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).
Expand Down
28 changes: 27 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 7 supported agents (Claude Code, Copilot, Cursor, Cline, Windsurf, Codex, OpenCode).
Owns: per-agent hook scripts and configuration files for 8 supported agents (Claude Code, Copilot, Cursor, Cline, Windsurf, Codex, OpenCode, Kimi).

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 @@ -40,6 +40,7 @@ Each agent subdirectory has its own README with hook-specific details:
- **[`windsurf/`](windsurf/README.md)** — Rules file (prompt-level), `.windsurfrules` workspace-scoped
- **[`codex/`](codex/README.md)** — Awareness document, `AGENTS.md` integration, `$CODEX_HOME` or `~/.codex/` location
- **[`opencode/`](opencode/README.md)** — TypeScript plugin, `zx` library, `tool.execute.before` event, in-place mutation
- **[`kimi/`](kimi/README.md)** — Native Rust binary hook, `PreToolUse` JSON format, `~/.kimi/config.toml` patching

## Supported Agents

Expand All @@ -54,6 +55,7 @@ Each agent subdirectory has its own README with hook-specific details:
| Windsurf | Custom instructions (rules file) | Prompt-level guidance | N/A |
| Codex CLI | AGENTS.md / instructions | Prompt-level guidance | N/A |
| OpenCode | TypeScript plugin (`tool.execute.before`) | In-place mutation | Yes |
| Kimi Code CLI | Rust binary (`rtk hook kimi`) | Transparent rewrite | Yes (`hookSpecificOutput`) |

## JSON Formats by Agent

Expand Down Expand Up @@ -145,6 +147,30 @@ Returns `{}` when no rewrite (Cursor requires JSON for all paths).

**No rewrite**: `{"decision": "allow"}`

### Kimi Code CLI (Rust Binary)

**Input** (stdin):
```json
{
"tool_name": "Shell",
"tool_input": { "command": "git status" }
}
```

**Output** (stdout, when rewritten):
```json
{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "allow",
"permissionDecisionReason": "RTK auto-rewrite",
"updatedInput": { "command": "rtk git status" }
}
}
```

**No rewrite**: empty stdout (exit 0)

### OpenCode (TypeScript Plugin)

Mutates `args.command` in-place via the zx library:
Expand Down
64 changes: 64 additions & 0 deletions hooks/kimi/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# Kimi Code CLI Hook

Native Rust binary hook for Kimi Code CLI. Reuses the Claude hook processor (`process_claude_payload`) since Kimi's `PreToolUse` JSON format and structured output are compatible.

## Installation

```bash
rtk init -g --kimi
```

This patches `~/.kimi/config.toml` to add a `PreToolUse` hook:

```toml
[[hooks]]
event = "PreToolUse"
matcher = "Shell"
command = "rtk hook kimi"
timeout = 5
```

## How It Works

1. Kimi fires `PreToolUse` before executing any `Shell` tool call
2. Kimi passes JSON via stdin to `rtk hook kimi`
3. RTK extracts the command, looks up the rewrite registry
4. If a match is found, RTK outputs structured JSON with the rewritten command
5. Kimi executes the rewritten command (e.g., `rtk git status`)

## JSON Format

**Input** (stdin):
```json
{
"tool_name": "Shell",
"tool_input": { "command": "git status" }
}
```

**Output** (stdout, when rewritten):
```json
{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "allow",
"permissionDecisionReason": "RTK auto-rewrite",
"updatedInput": { "command": "rtk git status" }
}
}
```

**No rewrite**: empty stdout (exit 0) — Kimi runs the original command unchanged.

## Files

| File | Purpose |
|------|---------|
| `~/.kimi/config.toml` | Hook registration (TOML `[[hooks]]` array) |
| `~/.kimi/KIMI.md` | Slim RTK awareness instructions |

## Uninstall

```bash
rtk init -g --kimi --uninstall
```
4 changes: 4 additions & 0 deletions src/hooks/constants.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,7 @@ pub const OPENCODE_PLUGIN_PATH: &str = ".config/opencode/plugins/rtk.ts";
pub const CURSOR_DIR: &str = ".cursor";
pub const CODEX_DIR: &str = ".codex";
pub const GEMINI_DIR: &str = ".gemini";
pub const KIMI_DIR: &str = ".kimi";

/// Native Rust hook command for Kimi Code CLI.
pub const KIMI_HOOK_COMMAND: &str = "rtk hook kimi";
94 changes: 94 additions & 0 deletions src/hooks/hook_cmd.rs
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,52 @@ fn audit_log_inner(action: &str, original: &str, rewritten: &str) -> Option<()>
.ok()
}

// ── Kimi Code CLI native hook ─────────────────────────────────

/// Run the Kimi Code CLI PreToolUse hook natively.
pub fn run_kimi() -> Result<()> {
let input = read_stdin_limited()?;

let input = input.trim();
if input.is_empty() {
return Ok(());
}

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}");
return Ok(());
}
};

match process_claude_payload(&v) {
PayloadAction::Rewrite {
cmd,
rewritten,
output,
} => {
audit_log("rewrite", &cmd, &rewritten);
let _ = writeln!(io::stdout(), "{output}");
}
PayloadAction::Skip { reason, cmd } => {
audit_log(reason, &cmd, "");
}
PayloadAction::Ignore => {}
}

Ok(())
}

#[cfg(test)]
fn run_kimi_inner(input: &str) -> Option<String> {
let v: Value = serde_json::from_str(input).ok()?;
match process_claude_payload(&v) {
PayloadAction::Rewrite { output, .. } => Some(output.to_string()),
_ => None,
}
}

// ── Claude Code native hook ────────────────────────────────────

enum PayloadAction {
Expand Down Expand Up @@ -905,4 +951,52 @@ mod tests {
"cargo test should be rewritable when not denied"
);
}

// --- Kimi handler ---

fn kimi_input(cmd: &str) -> String {
json!({
"tool_name": "Shell",
"tool_input": { "command": cmd }
})
.to_string()
}

#[test]
fn test_kimi_rewrite_git_status() {
let result = run_kimi_inner(&kimi_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_kimi_passthrough_no_output() {
assert!(run_kimi_inner(&kimi_input("htop")).is_none());
}

#[test]
fn test_kimi_heredoc_passthrough() {
assert!(run_kimi_inner(&kimi_input("cat <<EOF\nhello\nEOF")).is_none());
}

#[test]
fn test_kimi_already_rtk_passthrough() {
assert!(run_kimi_inner(&kimi_input("rtk git status")).is_none());
}

#[test]
fn test_kimi_json_output_structure() {
let result = run_kimi_inner(&kimi_input("git status")).unwrap();
let v: Value = serde_json::from_str(&result).unwrap();
let hook = &v["hookSpecificOutput"];

assert_eq!(hook["hookEventName"], PRE_TOOL_USE_KEY);
assert_eq!(hook["permissionDecisionReason"], "RTK auto-rewrite");
assert!(hook["updatedInput"].is_object());
assert!(hook["updatedInput"]["command"].is_string());
}
}
Loading