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
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,8 @@ 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 qoder # Qoder CLI (project-scoped)
rtk init -g --agent qoder # Qoder CLI (global)

# 2. Restart your AI tool, then test
git status # Automatically rewritten to rtk git status
Expand Down Expand Up @@ -350,7 +352,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 @@ -367,6 +369,7 @@ RTK supports 12 AI coding tools. Each integration transparently rewrites shell c
| **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) |
| **Qoder CLI** | `rtk init --agent qoder`<br>`rtk init -g --agent qoder` | PreToolUse hook — transparent rewrite (project or global) |

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
33 changes: 32 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, Qoder CLI).

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
- **[`qoder/`](qoder/README.md)** — Awareness document + Rust binary hook (`rtk hook qoder`), `PreToolUse` transparent rewrite, `settings.json` 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 |
| Qoder CLI | Rust binary (`rtk hook qoder`) | Transparent rewrite | Yes (`updatedInput`) |

## JSON Formats by Agent

Expand Down Expand Up @@ -93,6 +95,35 @@ Each agent subdirectory has its own README with hook-specific details:

Returns `{}` when no rewrite (Cursor requires JSON for all paths).

### Qoder CLI (Rust Binary)

Uses the same `PreToolUse` JSON protocol as Claude Code. The Rust binary (`rtk hook qoder`) reads from stdin and returns `updatedInput` for transparent command rewriting.

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

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

Returns nothing (empty stdout) when the command is non-Bash, already rtk-prefixed, or not optimizable.

### Copilot CLI (Rust Binary)

**Input** (stdin, camelCase, `toolArgs` is JSON-stringified):
Expand Down
25 changes: 25 additions & 0 deletions hooks/qoder/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Qoder CLI Hooks

> Part of [`hooks/`](../README.md) — see also [`src/hooks/`](../../src/hooks/README.md) for installation code

## Specifics

- Uses the `rtk hook qoder` Rust binary (not a shell script) — no `jq` dependency
- Qoder CLI uses the same `PreToolUse` JSON protocol as Claude Code (`tool_name`/`tool_input` + `updatedInput`)
- Hook config written to `~/.qoder/settings.json` (global) or `.qoder/settings.json` (project)
- Transparent rewrite via `updatedInput` — no denial, no retry

## How it works

1. Agent runs `git status` → `rtk hook qoder` intercepts via `PreToolUse`
2. `rtk hook` reads JSON from stdin, matches tool_name `Bash`
3. Returns `hookSpecificOutput.updatedInput.command = "rtk git status"`
4. Qoder CLI rewrites the command transparently — agent sees the filtered output directly

## Testing

```bash
# Simulate Qoder CLI PreToolUse input
echo '{"tool_name":"Bash","tool_input":{"command":"git status"}}' | rtk hook qoder
# Should output JSON with updatedInput.command = "rtk git status"
```
46 changes: 46 additions & 0 deletions hooks/qoder/rtk-awareness.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# RTK — Qoder CLI Integration

**Usage**: Token-optimized CLI proxy (60-90% savings on dev operations)

## What's automatic

The `PreToolUse` hook in Qoder CLI's settings.json intercepts raw Bash tool calls
and rewrites them through RTK transparently via `updatedInput`.

The `@RTK.md` reference in `AGENTS.md` provides prompt-level guidance for the LLM
to proactively use `rtk` prefix on shell commands.

No shell scripts, no `jq` dependency — `rtk hook qoder` is a native Rust binary.

## Meta commands (always use directly)

```bash
rtk gain # Token savings dashboard for this session
rtk gain --history # Per-command history with savings %
rtk discover # Scan session history for missed rtk opportunities
rtk proxy <cmd> # Run raw (no filtering) but still track it
```

## Installation verification

```bash
rtk --version # Should print: rtk X.Y.Z
rtk gain # Should show a dashboard (not "command not found")
which rtk # Verify correct binary path
```

> ⚠️ **Name collision**: If `rtk gain` fails, you may have `reachingforthejack/rtk`
> (Rust Type Kit) installed instead. Check `which rtk` and reinstall from rtk-ai/rtk.

## How the hook works

`rtk hook qoder` reads `PreToolUse` JSON from stdin and rewrites commands transparently:

1. Agent calls `Bash("git status")` → Qoder CLI fires `PreToolUse` hook
2. `rtk hook qoder` receives JSON, extracts `tool_input.command`
3. Returns `hookSpecificOutput.updatedInput.command = "rtk git status"`
4. Qoder CLI applies the updated input — agent sees RTK-filtered output directly

**Compound commands** (with `&&`, `||`, `|`, `;`) are handled by `rtk`'s internal
`rewrite_compound()` — each segment is rewritten independently, and pipe-incompatible
commands like `find`/`fd` are skipped automatically.
3 changes: 2 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 (4 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 (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.

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 Down Expand Up @@ -87,6 +87,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) |
| Qoder CLI (rtk hook qoder) | Yes | `permissionDecision: "ask"` — user prompted |

### Implementation

Expand Down
1 change: 1 addition & 0 deletions src/hooks/constants.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,4 @@ 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 QODER_DIR: &str = ".qoder";
87 changes: 87 additions & 0 deletions src/hooks/hook_cmd.rs
Original file line number Diff line number Diff line change
Expand Up @@ -397,6 +397,53 @@ fn run_claude_inner(input: &str) -> Option<String> {
}
}

/// Run the Qoder CLI PreToolUse hook natively.
///
/// Qoder CLI uses the same PreToolUse JSON protocol as Claude Code
/// (`tool_input.command` + `updatedInput`), so we reuse `process_claude_payload`.
pub fn run_qoder() -> 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_qoder_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,
}
}

// ── Cursor native hook ─────────────────────────────────────────

/// Run the Cursor Agent hook natively.
Expand Down Expand Up @@ -756,6 +803,46 @@ mod tests {
assert!(run_claude_inner(&input).is_none());
}

// --- Qoder handler ---

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

#[test]
fn test_qoder_rewrites_bash() {
let output = run_qoder_inner(&qoder_input("git status")).unwrap();
assert!(
output.contains("updatedInput"),
"Should contain updatedInput"
);
let v: Value = serde_json::from_str(&output).unwrap();
assert_eq!(
v["hookSpecificOutput"]["updatedInput"]["command"],
"rtk git status"
);
}

#[test]
fn test_qoder_passthrough_non_bash() {
let input = json!({"tool_name": "Read", "tool_input": {"file_path": "test"}}).to_string();
assert!(run_qoder_inner(&input).is_none());
}

#[test]
fn test_qoder_passthrough_already_rtk() {
assert!(run_qoder_inner(&qoder_input("rtk git status")).is_none());
}

#[test]
fn test_qoder_passthrough_echo() {
assert!(run_qoder_inner(&qoder_input("echo hello")).is_none());
}

// --- Cursor handler ---

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