CLI proxy that reduces LLM token consumption by filtering command output.
Inspired by rtk (14.6K stars, Rust). Reimplemented from scratch in Go with a unified filter interface, no shell injection vulnerability, and zero external dependencies.
LLM-powered coding tools (Claude Code, GitHub Copilot, Cursor) read CLI output to understand your project. But most CLI tools produce verbose, human-optimized output: ANSI colors, progress bars, passing test output, compilation status lines, git hints. LLMs don't need any of that noise.
rtk-go sits between your commands and your LLM, compressing output by 60-99% while preserving the information that actually matters for decision-making.
Before: git status → 200 tokens (headers, hints, whitespace)
After: rtk-go git status → 10 tokens ("[main] 3 modified, 1 untracked")
Savings: 95%
| Issue | rtk (Rust) | rtk-go |
|---|---|---|
| Architecture | 72 separate modules, no shared abstraction | Unified Filter interface, 11 implementations |
| Shell Safety | sh -c with user input (#640) |
exec.Command with explicit args — zero injection vectors |
| Dependencies | 22 crates | Zero external deps (Go stdlib only) |
| Streaming | Buffers all output (#222) | Buffered with fail-safe recovery |
| Telemetry | Opt-out device tracking | None |
| Build | Requires Rust toolchain | Single go build |
go install github.com/JSLEEKR/rtk-go/cmd/rtk-go@latestgit clone https://github.com/JSLEEKR/rtk-go.git
cd rtk-go
go build -o rtk-go ./cmd/rtk-go/# Instead of running commands directly, prefix with rtk-go:
rtk-go git status
rtk-go git diff
rtk-go git log
rtk-go grep -r "TODO" .
rtk-go find . -name "*.go"
rtk-go go test ./...
rtk-go go build ./...
# Pass through without filtering:
rtk-go --raw git log
# Show savings report:
rtk-go --report git diffrtk-go includes 11 filters covering the most common CLI tools used in development:
Parses verbose git status into a compact summary with file counts by change type.
Input (200 tokens):
On branch main
Your branch is up to date with 'origin/main'.
Changes to be committed:
(use "git restore --staged <file>..." to unstage)
new file: README.md
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
modified: src/main.go
modified: src/util.go
Untracked files:
(use "git add <file>..." to include in what will be committed)
newfile.txt
Output (15 tokens):
[main] 1 staged, 2 modified, 1 untracked
Staged:
README.md
Modified:
src/main.go
src/util.go
Untracked:
newfile.txt
Limits diff hunks to 100 lines per file with summary statistics and recovery hints.
Output:
diff --git a/main.go b/main.go
@@ -1,5 +1,6 @@
[first 100 lines of changes]
... [truncated, 100+ lines in main.go]
--- 3 file(s) changed, +45 -12 (1 file(s) truncated at 100 lines)
Caps at 10 commits, strips trailers (Signed-off-by, Co-authored-by, etc.).
Output:
commit abc123...
Author: Developer <dev@example.com>
Add new feature
... [showing 10 of 85 commits]
Groups results by file with per-file limits (25) and total limits (200).
Output:
## src/main.go (3 matches)
10: func main() {
25: func helper() {
40: func process() {
## src/util.go (1 matches)
5: func utility() {
--- 4 matches in 2 files
Groups by parent directory with extension summary and budget-aware limiting.
Output:
src/
main.go
util.go
tests/
main_test.go
--- 3F 2D: .go(3)
Filters noise directories (node_modules, .git, pycache, etc.) and provides item counts.
Output:
src
tests
go.mod
README.md
--- 4 items (3 noise dirs hidden)
Parses both JSON (go test -json) and verbose output. Hides passing tests, shows only failures.
Input (500 tokens):
=== RUN TestA
--- PASS: TestA (0.01s)
=== RUN TestB
--- PASS: TestB (0.02s)
=== RUN TestC
main_test.go:15: expected 1 got 2
--- FAIL: TestC (0.03s)
FAIL myapp 0.5s
Output (30 tokens):
FAIL: 1/3 tests failed
--- FAIL: TestC (0.03s)
main_test.go:15: expected 1 got 2
State machine parser that extracts failure blocks and summary lines. Hides all passing tests.
Output:
========================= 2 passed, 1 failed in 0.5s =========================
1 failure(s):
_________________________________ test_bad _________________________________
def test_bad():
> assert 1 == 2
E assert 1 == 2
Extracts test summary lines and failure blocks from JavaScript test runners.
Strips compilation progress lines ("Compiling...", "Building..."), keeps only errors and warnings.
Input (100 tokens):
Compiling serde v1.0.0
Compiling tokio v1.0.0
Compiling myapp v0.1.0
error[E0308]: mismatched types
--> src/main.rs:10:5
Output (20 tokens):
1 error(s):
error[E0308]: mismatched types
--> src/main.rs:10:5
(3 progress lines hidden)
For unrecognized commands: strips ANSI escape codes, collapses blank lines, smart truncation (preserves head/tail).
The core design advantage over rtk. Every filter implements the same interface:
type Filter interface {
Name() string
Match(cmd string, args []string) bool
Apply(output string, exitCode int, cfg *config.FilterConfig) string
}This replaces rtk's 72 ad-hoc Rust modules with a clean, testable abstraction.
CLI Input (e.g., "rtk-go git diff")
│
▼
Command Parser ─── identify command + args
│
▼
Filter Registry ─── lookup matching filter by command name
│
▼
exec.Command ─── run actual command, capture stdout/stderr
│ (NO shell interpolation — prevents injection)
▼
Filter.Apply() ─── compress output using domain-specific heuristics
│
▼
Token Counter ─── track input/output token counts (chars/4)
│
▼
Compressed Output ─── to stdout (savings report to stderr)
If any filter panics or errors, rtk-go returns the raw unfiltered output. You never lose data:
func applyFilterSafe(f Filter, output string, exitCode int) (result string) {
defer func() {
if r := recover(); r != nil {
result = output // fail-safe: return raw output
}
}()
return f.Apply(output, exitCode)
}rtk (Rust) has a critical vulnerability (#640): user input flows through sh -c enabling shell injection. rtk-go uses exec.Command with explicit argument arrays:
// SECURITY: exec.Command with explicit args — NO shell interpolation
cmd := exec.Command(cmdName, args...)The underlying command's exit code is always preserved and propagated. This is critical for CI/CD pipelines.
rtk-go reads configuration from ~/.config/rtk-go/config.yaml:
# rtk-go configuration
max_lines: 300
filters:
grep_max_results: 200
grep_max_per_file: 25
git_status_max: 15
git_diff_max_lines: 100
git_log_max_commits: 10
find_max_results: 100
test_max_failures: 10
# Disable specific filters (uses generic fallback instead)
disabled:
- build| Key | Default | Description |
|---|---|---|
max_lines |
300 | Global max output lines before truncation |
grep_max_results |
200 | Max total grep matches shown |
grep_max_per_file |
25 | Max grep matches per file |
git_status_max |
15 | Max staged/modified files shown |
git_diff_max_lines |
100 | Max diff lines per file section |
git_log_max_commits |
10 | Max commits shown in git log |
find_max_results |
100 | Max files shown in find output |
test_max_failures |
10 | Max test failures shown in detail |
rtk-go uses the chars / 4 heuristic for token estimation:
tokens = ceil(characters / 4)
savings = (input_tokens - output_tokens) / input_tokens × 100%
This approximation is accurate to within ~20% for English text and trades precision for zero-overhead measurement. Note: Savings percentages shown are estimates based on this heuristic, not exact tokenizer counts. Actual token savings may vary by model and encoding. The savings report is displayed on stderr when --report is used:
--- rtk-go: git-status | 250→12 tokens (95% saved)
Use --report for a session summary:
=== rtk-go Token Savings Report ===
Commands: 5
Input tokens: 3250
Output tokens: 450
Saved: 2800 (86.2%)
By filter:
git-status 2 commands, 1800 tokens saved
git-diff 1 commands, 600 tokens saved
grep 1 commands, 300 tokens saved
go-test 1 commands, 100 tokens saved
rtk-go/
├── cmd/rtk-go/
│ └── main.go # CLI entry point (stdlib flag)
├── internal/
│ ├── config/
│ │ ├── config.go # YAML config loading (stdlib only)
│ │ └── config_test.go
│ ├── filter/
│ │ ├── filter.go # Filter interface + Registry
│ │ ├── filter_test.go # Registry lookup tests
│ │ ├── git.go # Git filters (status, diff, log)
│ │ ├── git_test.go
│ │ ├── grep.go # Grep, find, ls filters
│ │ ├── grep_test.go
│ │ ├── test.go # Test runner filters (go, pytest, npm)
│ │ ├── test_test.go
│ │ ├── build.go # Build output filters
│ │ ├── build_test.go
│ │ ├── generic.go # Fallback filter
│ │ └── generic_test.go
│ ├── proxy/
│ │ ├── proxy.go # Command execution + filter pipeline
│ │ └── proxy_test.go
│ ├── report/
│ │ ├── report.go # Token savings reporting
│ │ └── report_test.go
│ └── token/
│ ├── counter.go # Token counting heuristic
│ └── counter_test.go
├── go.mod
└── README.md
| Metric | rtk (Rust) | rtk-go |
|---|---|---|
| Language | Rust | Go |
| Source files | 72 modules | 11 filter files |
| External deps | 22 crates | 0 |
| Binary size | ~10MB | ~5MB |
| Filter architecture | Ad-hoc per module | Unified interface |
| Shell safety | sh -c (injection risk) |
exec.Command (safe) |
| Telemetry | Device tracking (opt-out) | None |
| Test count | Unknown | 156 |
| Streaming support | Buffered only | Buffered with fail-safe |
| Config format | TOML | YAML (stdlib parser) |
| Windows | Incomplete hooks | First-class support |
-
Unified Filter Interface — Not 72 separate modules. One
Filterinterface, clean registry lookup. Easy to add new filters. -
No Shell Injection — rtk passes user input through
sh -c, enabling command injection. rtk-go usesexec.Commandwith explicit argument arrays. No shell involved. -
Zero External Dependencies — Everything uses Go stdlib. The YAML parser is built-in (simple key-value format). No supply chain risk.
-
Fail-Safe Design — If any filter panics, raw output is returned unchanged. You never lose data.
-
Simpler Codebase — ~2100 lines of source code (excluding tests) vs thousands of lines of Rust across 72 files. Easier to audit, maintain, and contribute to.
# Run tests
go test ./... -v
# Build
go build -o rtk-go ./cmd/rtk-go/
# Vet
go vet ./...
# Run with verbose output
./rtk-go --report git statusMIT