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
2 changes: 1 addition & 1 deletion .github/hooks/rtk-rewrite.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"PreToolUse": [
{
"type": "command",
"command": "rtk hook",
"command": "rtk hook copilot",
"cwd": ".",
"timeout": 5
}
Expand Down
27 changes: 26 additions & 1 deletion INSTALL.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,10 @@ rtk gain # MUST show token savings, not "command not found"
│ Hook + RTK.md (~10 tokens in context)
│ Commands auto-rewritten transparently
├─ GitHub Copilot everywhere → rtk init -g --agent copilot
│ Global Copilot instructions in ~/.copilot/copilot-instructions.md
│ Helps both Copilot CLI and VS Code Copilot Chat use rtk
├─ YES, minimal → rtk init -g --hook-only
│ Hook only, nothing added to CLAUDE.md
│ Zero tokens in context
Expand All @@ -93,7 +97,8 @@ rtk gain # MUST show token savings, not "command not found"

```bash
rtk init -g
# → Installs hook to ~/.claude/hooks/rtk-rewrite.sh
# → Installs hook to ~/.claude/hooks/rtk-rewrite.sh (macOS/Linux)
# or ~/.claude/hooks/rtk-rewrite.cmd (Windows)
# → Creates ~/.claude/RTK.md (10 lines, meta commands only)
# → Adds @RTK.md reference to ~/.claude/CLAUDE.md
# → Prompts: "Patch settings.json? [y/N]"
Expand All @@ -109,6 +114,26 @@ rtk init --show # Check hook is installed and executable

**Token savings**: ~99.5% reduction (2000 tokens → 10 tokens in context)

### GitHub Copilot Setup

**Best for: GitHub Copilot CLI and VS Code Copilot Chat**

```bash
# Global instructions for all Copilot sessions
rtk init -g --agent copilot

# Repository-local instructions + PreToolUse hook
rtk init --agent copilot
```

Global mode writes `~/.copilot/copilot-instructions.md`, which Copilot CLI and VS Code Copilot Chat both load automatically.

Repository mode writes:
- `.github/copilot-instructions.md`
- `.github/hooks/rtk-rewrite.json`

The repository hook runs `rtk hook copilot`, which is cross-platform and works on Windows without `bash` or `jq`.

**What is settings.json?**
Claude Code's hook registry. RTK adds a PreToolUse hook that rewrites commands transparently. Without this, Claude won't invoke the hook automatically.

Expand Down
43 changes: 43 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,7 @@ rtk curl <url> # Auto-detect JSON + schema
rtk wget <url> # Download, strip progress bars
rtk summary <long command> # Heuristic summary
rtk proxy <command> # Raw passthrough + tracking
rtk pwsh -Command pwd # PowerShell builtins/aliases via RTK
```

### Token Savings Analytics
Expand All @@ -227,6 +228,7 @@ rtk gain --all --format json # JSON export for dashboards

rtk discover # Find missed savings opportunities
rtk discover --all --since 7 # All projects, last 7 days
# Includes Claude Code + Copilot shell sessions

rtk session # Show RTK adoption across recent sessions
```
Expand Down Expand Up @@ -285,8 +287,49 @@ rtk init -g --hook-only # Hook only, no RTK.md
rtk init --show # Verify installation
```

On Windows, RTK installs `~/.claude/hooks/rtk-rewrite.cmd`; on macOS/Linux it installs `~/.claude/hooks/rtk-rewrite.sh`.

After install, **restart Claude Code**.

## GitHub Copilot Support

RTK supports both GitHub Copilot CLI and VS Code Copilot Chat.

**Global instructions for Copilot (recommended for VS Code):**
```bash
rtk init -g --agent copilot
```

This writes `~/.copilot/copilot-instructions.md`, which Copilot CLI and VS Code Copilot Chat both load automatically.

**Repository hook + instructions (for a specific repo):**
```bash
rtk init --agent copilot
```

This creates:
- `.github/copilot-instructions.md`
- `.github/hooks/rtk-rewrite.json`

The hook uses the native Rust processor directly:

```json
{
"hooks": {
"PreToolUse": [
{
"type": "command",
"command": "rtk hook copilot",
"cwd": ".",
"timeout": 5
}
]
}
}
```

This is cross-platform and works on Windows without shell wrappers or `jq`.

## Gemini CLI Support (Global)

RTK supports Gemini CLI via a native Rust hook processor. The hook intercepts `run_shell_command` tool calls and rewrites them to `rtk` equivalents using the same rewrite engine as Claude Code.
Expand Down
9 changes: 9 additions & 0 deletions build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,15 @@ use std::fs;
use std::path::Path;

fn main() {
#[cfg(windows)]
{
// Clap + the full command graph can exceed the default 1 MiB Windows
// main-thread stack during process startup. Reserve a larger stack for
// the CLI binary so `rtk.exe --version`, `--help`, and hook entry
// points start reliably without requiring ad-hoc RUSTFLAGS.
println!("cargo:rustc-link-arg=/STACK:8388608");
}

let filters_dir = Path::new("src/filters");
let out_dir = std::env::var("OUT_DIR").expect("OUT_DIR must be set by Cargo");
let dest = Path::new(&out_dir).join("builtin_filters.toml");
Expand Down
40 changes: 40 additions & 0 deletions hooks/copilot-global-rtk-awareness.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# RTK - Copilot Global Instructions

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

This file is intended for `~/.copilot/copilot-instructions.md` so GitHub Copilot CLI and VS Code Copilot Chat can load RTK guidance globally.

## Golden Rule

Always prefer `rtk` for shell commands that produce verbose output.

Examples:

```bash
rtk git status
rtk git diff
rtk cargo test
rtk npm run build
rtk pytest -q
rtk docker ps
```

## Meta Commands

```bash
rtk gain # Show token savings analytics
rtk gain --history # Show command usage history with savings
rtk discover # Analyze sessions for missed RTK usage
rtk proxy <cmd> # Run raw command without filtering
```

## Verification

```bash
rtk --version
rtk gain
where rtk # Windows
which rtk # macOS/Linux
```

⚠️ **Name collision**: If `rtk gain` fails, you may have the wrong `rtk` installed.
14 changes: 7 additions & 7 deletions hooks/copilot-rtk-awareness.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
The `.github/copilot-instructions.md` file is loaded at session start by both Copilot CLI and VS Code Copilot Chat.
It instructs Copilot to prefix commands with `rtk` automatically.

The `.github/hooks/rtk-rewrite.json` hook adds a `PreToolUse` safety net via `rtk hook` —
The `.github/hooks/rtk-rewrite.json` hook adds a `PreToolUse` safety net via `rtk hook copilot` —
a cross-platform Rust binary that intercepts raw bash tool calls and rewrites them.
No shell scripts, no `jq` dependency, works on Windows natively.

Expand All @@ -33,21 +33,21 @@ which rtk # Verify correct binary path

## How the hook works

`rtk hook` reads `PreToolUse` JSON from stdin, detects the agent format, and responds appropriately:
`rtk hook copilot` reads `PreToolUse` JSON from stdin, detects the agent format, and responds appropriately:

**VS Code Copilot Chat** (supports `updatedInput` — transparent rewrite, no denial):
1. Agent runs `git status` → `rtk hook` intercepts via `PreToolUse`
2. `rtk hook` detects VS Code format (`tool_name`/`tool_input` keys)
1. Agent runs `git status` → `rtk hook copilot` intercepts via `PreToolUse`
2. `rtk hook copilot` detects VS Code format (`tool_name`/`tool_input` keys)
3. Returns `hookSpecificOutput.updatedInput.command = "rtk git status"`
4. Agent runs the rewritten command silently — no denial, no retry

**GitHub Copilot CLI** (deny-with-suggestion — CLI ignores `updatedInput` today, see [issue #2013](https://github.com/github/copilot-cli/issues/2013)):
1. Agent runs `git status` → `rtk hook` intercepts via `PreToolUse`
2. `rtk hook` detects Copilot CLI format (`toolName`/`toolArgs` keys)
1. Agent runs `git status` → `rtk hook copilot` intercepts via `PreToolUse`
2. `rtk hook copilot` detects Copilot CLI format (`toolName`/`toolArgs` keys)
3. Returns `permissionDecision: deny` with reason: `"Token savings: use 'rtk git status' instead"`
4. Copilot reads the reason and re-runs `rtk git status`

When Copilot CLI adds `updatedInput` support, only `rtk hook` needs updating — no config changes.
When Copilot CLI adds `updatedInput` support, only `rtk hook copilot` needs updating — no config changes.

## Integration comparison

Expand Down
12 changes: 12 additions & 0 deletions hooks/rtk-rewrite.cmd
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
@echo off
REM rtk-hook-version: 2
REM RTK Claude Code hook — rewrites commands to use rtk for token savings.
REM Windows variant: delegate to the native Rust Claude/Copilot hook processor.

where rtk >nul 2>nul
if errorlevel 1 (
echo [rtk] WARNING: rtk is not installed or not in PATH. Hook cannot rewrite commands. Install: https://github.com/rtk-ai/rtk#installation 1>&2
exit /b 0
)

rtk hook copilot
73 changes: 55 additions & 18 deletions src/discover/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@ pub mod rules;

use anyhow::Result;
use std::collections::HashMap;
use std::path::PathBuf;

use provider::{ClaudeProvider, SessionProvider};
use provider::{ClaudeProvider, CopilotProvider, SessionProvider};
use registry::{
category_avg_tokens, classify_command, has_rtk_disabled_prefix, split_command_chain,
strip_disabled_prefix, Classification,
Expand All @@ -30,6 +31,21 @@ struct UnsupportedBucket {
example: String,
}

#[derive(Clone, Copy)]
enum SessionSource {
Claude,
Copilot,
}

impl SessionSource {
fn label(self) -> &'static str {
match self {
SessionSource::Claude => "claude",
SessionSource::Copilot => "copilot",
}
}
}

pub fn run(
project: Option<&str>,
all: bool,
Expand All @@ -38,27 +54,41 @@ pub fn run(
format: &str,
verbose: u8,
) -> Result<()> {
let provider = ClaudeProvider;
let claude_provider = ClaudeProvider;
let copilot_provider = CopilotProvider;

// Determine project filter
let project_filter = if all {
None
let (claude_filter, copilot_filter) = if all {
(None, None)
} else if let Some(p) = project {
Some(p.to_string())
(Some(p.to_string()), Some(p.to_string()))
} else {
// Default: current working directory
let cwd = std::env::current_dir()?;
let cwd_str = cwd.to_string_lossy().to_string();
let encoded = ClaudeProvider::encode_project_path(&cwd_str);
Some(encoded)
(
Some(ClaudeProvider::encode_project_path(&cwd_str)),
Some(cwd_str),
)
};

let sessions = provider.discover_sessions(project_filter.as_deref(), Some(since_days))?;
let mut sessions: Vec<(SessionSource, PathBuf)> = Vec::new();

match claude_provider.discover_sessions(claude_filter.as_deref(), Some(since_days))? {
found => sessions.extend(found.into_iter().map(|p| (SessionSource::Claude, p))),
}

match copilot_provider.discover_sessions(copilot_filter.as_deref(), Some(since_days)) {
Ok(found) => sessions.extend(found.into_iter().map(|p| (SessionSource::Copilot, p))),
Err(err) => {
if verbose > 0 {
eprintln!("Skipping Copilot sessions: {err}");
}
}
}

if verbose > 0 {
eprintln!("Scanning {} session files...", sessions.len());
for s in &sessions {
eprintln!(" {}", s.display());
for (source, path) in &sessions {
eprintln!(" [{}] {}", source.label(), path.display());
}
}

Expand All @@ -70,12 +100,21 @@ pub fn run(
let mut supported_map: HashMap<&'static str, SupportedBucket> = HashMap::new();
let mut unsupported_map: HashMap<String, UnsupportedBucket> = HashMap::new();

for session_path in &sessions {
let extracted = match provider.extract_commands(session_path) {
for (source, session_path) in &sessions {
let extracted = match source {
SessionSource::Claude => claude_provider.extract_commands(session_path),
SessionSource::Copilot => copilot_provider.extract_commands(session_path),
};
let extracted = match extracted {
Ok(cmds) => cmds,
Err(e) => {
if verbose > 0 {
eprintln!("Warning: skipping {}: {}", session_path.display(), e);
eprintln!(
"Warning: skipping [{}] {}: {}",
source.label(),
session_path.display(),
e
);
}
parse_errors += 1;
continue;
Expand Down Expand Up @@ -126,7 +165,7 @@ pub fn run(

// Estimate tokens for this command
let output_tokens = if let Some(len) = ext_cmd.output_len {
// Real: from tool_result content length
// Real: from tool_result / tool execution content length
len / 4
} else {
// Fallback: category average
Expand Down Expand Up @@ -156,11 +195,9 @@ pub fn run(
bucket.count += 1;
}
Classification::Ignored => {
// Check if it starts with "rtk "
if part.trim().starts_with("rtk ") {
already_rtk += 1;
}
// Otherwise just skip
}
}
}
Expand Down
Loading
Loading