A CLI proxy that intercepts and filters shell output through declarative YAML pipelines, reducing token consumption by 60-90% for AI coding assistants like Claude Code, Cursor, and Copilot. It works transparently by dropping verbose command output before it reaches the LLM's context window, with built-in filters for common development tools like git, go, cargo, and kubectl.
CLI proxy that reduces LLM token usage by 60-90%. Declarative YAML filters for Claude Code, Cursor, Copilot, Gemini. rtk alternative in Go.
README
snip - Reduce LLM Token Usage by 60-90%
CLI proxy that filters shell output before it reaches your AI coding assistant's context window. Works with Claude Code, Cursor, Copilot, Gemini CLI, Windsurf, Cline, Codex, Kilo Code, Antigravity, Aider, and any tool that runs shell commands.
AI coding agents burn tokens on verbose shell output that adds zero signal. A passing go test produces hundreds of lines the LLM will never use. git log dumps full commit metadata when a one-liner per commit suffices.
snip sits between your AI tool and the shell, filtering output through declarative YAML pipelines. Write a YAML file, drop it in a folder, done. The extensible LLM token optimizer: filters are YAML data files, not compiled code.
snip — Token Savings Report
══════════════════════════════
Commands filtered 128
Tokens saved 2.3M
Avg savings 99.8%
Efficiency Elite
Total time 725.9s
███████████████████░ 100%
14-day trend ▁█▇
Top commands by tokens saved
Command Runs Saved Savings Impact
───────────────────────── ──── ────── ─────── ────────────
go test ./... 8 806.2K 99.8% ████████████
go test ./pkg/... 3 482.9K 99.8% ███████░░░░░
go test ./... -count=1 3 482.0K 99.8% ███████░░░░░
Measured on a real Claude Code session — 128 commands, 2.3M tokens saved.
Quick Start
# Quick install (macOS/Linux)
curl -fsSL https://raw.githubusercontent.com/edouard-claude/snip/master/install.sh | sh
# Or via Homebrew
brew install edouard-claude/tap/snip
# Or with Go
go install github.com/edouard-claude/snip/cmd/snip@latest
# Then hook into Claude Code
snip init
# That's it. Every shell command Claude runs now goes through snip.
How It Works
Before — Claude Code sees this (689 tokens):
$ go test ./...
ok github.com/edouard-claude/snip/internal/cli 3.728s coverage: 14.4% of statements
ok github.com/edouard-claude/snip/internal/config 2.359s coverage: 65.0% of statements
ok github.com/edouard-claude/snip/internal/display 1.221s coverage: 72.6% of statements
ok github.com/edouard-claude/snip/internal/engine 1.816s coverage: 47.9% of statements
ok github.com/edouard-claude/snip/internal/filter 4.306s coverage: 72.3% of statements
ok github.com/edouard-claude/snip/internal/initcmd 2.981s coverage: 59.1% of statements
ok github.com/edouard-claude/snip/internal/tee 0.614s coverage: 70.6% of statements
ok github.com/edouard-claude/snip/internal/tracking 5.355s coverage: 75.0% of statements
ok github.com/edouard-claude/snip/internal/utils 5.515s coverage: 100.0% of statements
After — snip returns this (16 tokens):
10 passed, 0 failed
That's 97.7% fewer tokens. The LLM gets the same signal — all tests pass — without the noise.
┌─────────────┐ ┌─────────────────┐ ┌──────────────┐ ┌────────────┐
│ Claude Code │────>│ snip intercept │────>│ run command │────>│ filter │
│ runs git │ │ match filter │ │ capture I/O │ │ pipeline │
└─────────────┘ └─────────────────┘ └──────────────┘ └─────┬──────┘
│
┌─────────────────┐ ┌──────────────┐ │
│ Claude Code │<────│ track savings│<──────────┘
│ sees filtered │ │ in SQLite │
└─────────────────┘ └──────────────┘
No filter match? The command passes through unchanged — zero overhead.
Token Savings by Command
| Command | Before | After | Reduction |
|---|---|---|---|
cargo test |
591 tokens | 5 tokens | 99.2% |
go test ./... |
689 tokens | 16 tokens | 97.7% |
git log |
371 tokens | 53 tokens | 85.7% |
git status |
112 tokens | 16 tokens | 85.7% |
git diff |
355 tokens | 66 tokens | 81.4% |
Stop wasting tokens on noise. snip gives the LLM the same signal in a fraction of the context window.
Installation
Homebrew (recommended)
brew install edouard-claude/tap/snip
From GitHub Releases
Download the latest binary for your platform from Releases.
# macOS (Apple Silicon)
curl -Lo snip.tar.gz https://github.com/edouard-claude/snip/releases/latest/download/snip_$(curl -s https://api.github.com/repos/edouard-claude/snip/releases/latest | grep tag_name | cut -d'"' -f4 | tr -d v)_darwin_arm64.tar.gz
tar xzf snip.tar.gz && mv snip /usr/local/bin/
From source
go install github.com/edouard-claude/snip/cmd/snip@latest
Or build locally:
git clone https://github.com/edouard-claude/snip.git
cd snip && make install
Requires Go 1.25+.
Supported AI Tools
snip integrates with every major AI coding assistant. One binary, universal compatibility.
| Tool | Install | Method |
|---|---|---|
| Claude Code | snip init |
PreToolUse hook (native) |
| Cursor | snip init --agent cursor |
beforeShellExecution hook (native) |
| GitHub Copilot | snip init --agent copilot |
.github/copilot-instructions.md |
| Gemini CLI | snip init --agent gemini |
GEMINI.md prompt injection |
| Codex (OpenAI) | snip init --agent codex |
PreToolUse hook (native) |
| Pi (pi.dev) | snip init --agent pi |
PreToolUse hook (via pi-hooks) |
| Grok Build (xAI) | snip init --agent grok |
PreToolUse hook (deny + re-run suggestion) |
| Windsurf | snip init --agent windsurf |
.windsurfrules prompt injection |
| Cline / Roo Code | snip init --agent cline |
.clinerules prompt injection |
| Kilo Code | snip init --agent kilocode |
.kilocode/rules/ prompt injection |
| Antigravity | snip init --agent antigravity |
.agents/rules/ prompt injection |
| OpenCode | opencode-snip plugin | tool.execute.before hook |
| OpenClaw | openclaw plugins install openclaw-snip |
plugin |
| Aider | shell aliases | prefix commands with snip |
Claude Code
snip init
This installs a PreToolUse hook that transparently rewrites supported commands. Claude Code never sees the substitution -- it receives compressed output as if the original command produced it.
Supported commands: 132 filters covering git, go, cargo, npm, yarn, pnpm, docker, kubectl, terraform, aws, gh, and 80+ more tools.
snip init --uninstall # remove the hook
Cursor
snip init --agent cursor
This patches ~/.cursor/hooks.json with a beforeShellExecution hook. Works the same way as Claude Code.
snip init --agent cursor --uninstall # remove the hook
Pi (pi.dev)
snip init --agent pi
This patches ~/.pi/agent/settings.json with a PreToolUse entry matching the bash tool. The runtime hook is interpreted by the community extension @hsingjui/pi-hooks, which mirrors Claude Code's hookSpecificOutput format (including command rewriting via updatedInput). Install it once:
pi install npm:@hsingjui/pi-hooks
Then run /reload (or restart Pi). Once active, snip rewrites supported commands transparently.
snip init --agent pi --uninstall # remove the hook
Grok Build (xAI)
snip init --agent grok
This writes ~/.grok/hooks/snip.json with a PreToolUse hook matching the shell tool. Grok Build hooks cannot rewrite commands in place (the hook contract is allow/deny only), so snip denies matched commands with a re-run suggestion ("…/snip" run -- <command>). Non-matching commands pass through untouched, and the hook is fail-open: if snip breaks, commands simply run unfiltered.
Prefer prompt injection instead? Grok Build reads AGENTS.md natively:
snip init --agent grok --mode prompt # creates AGENTS.md
snip init --agent grok --uninstall # remove the hook
AGENTS.md is shared with Codex, so --uninstall never deletes it; it prints a reminder instead.
Codex
snip init --agent codex
This patches ~/.codex/hooks.json with a native PreToolUse hook. Supported shell commands are transparently rewritten through snip with Codex's updatedInput contract, so the model runs commands normally and sees only the filtered output.
For safety, mixed commands containing unsupported segments, pipelines with uninspected tails, and command substitutions pass through unchanged so Codex keeps its native permission flow.
Use snip proxy -- <command> when full unfiltered output is required.
snip init --agent codex --uninstall
For older Codex releases without PreToolUse input rewriting, use the legacy project-scoped prompt integration:
snip init --agent codex --mode prompt
Copilot / Gemini / Windsurf / Cline / Kilo Code / Antigravity
snip init --agent copilot # creates .github/copilot-instructions.md
snip init --agent gemini # creates GEMINI.md
snip init --agent windsurf # creates .windsurfrules
snip init --agent cline # creates .clinerules
snip init --agent kilocode # creates .kilocode/rules/snip-rules.md
snip init --agent antigravity # creates .agents/rules/snip-rules.md
These agents use prompt injection: a markdown file instructs the LLM to prefix shell commands with snip. Project-scoped (created in the current directory).
OpenCode
Install the opencode-snip plugin by adding it to your OpenCode config (~/.config/opencode/opencode.json):
{
"$schema": "https://opencode.ai/config.json",
"plugin": ["opencode-snip@latest"]
}
The plugin uses the tool.execute.before hook to automatically prefix all commands with snip. Commands not supported by snip pass through unchanged.
OpenClaw
openclaw plugins install openclaw-snip
Aider
Use shell aliases to route commands through snip:
# Add to ~/.bashrc or ~/.zshrc
alias git="snip git"
alias go="snip go"
alias cargo="snip cargo"
Or instruct the LLM via system prompt to prefix commands with snip.
Standalone
snip works without any AI tool:
snip git log -10
snip go test ./...
snip gain # token savings report
Usage
snip <command> [args] # filter a command
snip gain # full dashboard (summary + sparkline + top commands)
snip gain --daily # daily breakdown
snip gain --weekly # weekly breakdown
snip gain --monthly # monthly breakdown
snip gain --top 10 # top N commands by tokens saved
snip gain --history 20 # last 20 commands
snip gain --no-truncate # disable command truncation
snip gain --json # machine-readable output
snip gain --csv # CSV export
snip discover # find missed savings in Claude Code history
snip discover --since 30 # scan last 30 days
snip discover --all # scan all projects
snip -v <command> # verbose mode (show filter details)
snip proxy <command> # force passthrough (no filtering)
snip proxy -- <command> # same, with explicit separator (like run/check)
snip config # show config
snip init # install Claude Code hook
snip init --agent cursor # install Cursor hook
snip init --agent pi # install Pi (pi.dev) hook
snip init --agent copilot # install Copilot integration
snip init --agent gemini # install Gemini CLI integration
snip init --agent kilocode # install Kilo Code integration
snip init --agent antigravity # install Antigravity integration
snip init --uninstall # remove hook
Filters
Filters are declarative YAML files. The binary is the engine, filters are data — the two evolve independently.
name: "git-log"
version: 1
description: "Condense git log to hash + message"
match:
command: "git"
subcommand: "log"
exclude_flags: ["--format", "--pretty", "--oneline"]
inject:
args: ["--pretty=format:%h %s (%ar) <%an>", "--no-merges"]
defaults:
"-n": "10"
pipeline:
- action: "keep_lines"
pattern: "\\S"
- action: "truncate_lines"
max: 80
- action: "format_template"
template: "{{.count}} commits:\n{{.lines}}"
on_error: "passthrough"
match.subcommand can be a scalar string (as above) or a list of exact subcommands:
match:
command: "npm"
subcommand: ["install", "add", "i"]
If subcommand is omitted, the filter matches every subcommand for that command. To match only a bare command invocation, include an explicit empty string, for example subcommand: ["", "install"] to match yarn and yarn install without matching yarn why.
132 Built-in Filters
snip ships with 132 declarative YAML filters covering all major developer tools:
| Category | Filters |
|---|---|
| Git (12) | status, log, diff, show, add, commit, push, pull, branch, fetch, stash, worktree |
| GitHub CLI (3) | gh pr, gh issue, gh run |
| Go (4) | go test, go build, go vet, golangci-lint |
| Rust (7) | cargo test/build/check/clippy/install/nextest, rustc |
| Python (8) | pytest, ruff, mypy, basedpyright, ty, pip, poetry, uv |
| JavaScript/TypeScript (17) | jest, vitest, eslint, tsc, biome, oxlint, prettier, next, playwright, nx, turbo, npm, npx, yarn, pnpm, prisma |
| Ruby (6) | rspec, rubocop, rake, bundle, rails migrate, rails routes |
| .NET (3) | dotnet build/test/format |
| Docker/K8s (7) | docker build/ps/images/logs/compose, kubectl get/logs |
| Cloud/Infra (6) | terraform, tofu, helm, ansible-playbook, gcloud, aws |
| Build tools (13) | make, gcc, g++, gradle, gradlew, mvn, swift, xcodebuild, just, task, pio, trunk, mise |
| Files/Search (7) | ls, find, grep, rg, diff, wc, tree |
| Linting (5) | shellcheck, hadolint, markdownlint, yamllint, pre-commit |
| Package managers (4) | brew, composer, poetry, uv |
| System/Network (14) | curl, wget, psql, jq, ping, ssh, rsync, df, du, ps, systemctl, iptables, stat, fail2ban |
| Other (11) | jira, jj, yadm, gt, ollama, sops, skopeo, shopify, quarto, liquibase, spring-boot |
Run snip discover to see which of your commands already have filters.
19 Pipeline Actions
| Action | Description |
|---|---|
keep_lines |
Keep lines matching regex |
remove_lines |
Remove lines matching regex |
truncate_lines |
Truncate lines to max length |
strip_ansi |
Remove ANSI escape codes |
head / tail |
Keep first/last N lines |
group_by |
Group lines by regex capture |
dedup |
Deduplicate with optional normalization |
json_extract |
Extract fields from JSON |
json_schema |
Infer schema from JSON |
ndjson_stream |
Process newline-delimited JSON |
regex_extract |
Extract regex captures |
state_machine |
Multi-state line processing |
aggregate |
Count pattern matches |
format_template |
Go template formatting |
compact_path |
Shorten file paths (see caveat below) |
replace |
Regex find and replace |
match_output |
Conditional short-circuit (return message if pattern matches) |
on_empty |
Return message if output is empty |
compact_pathemits paths that may not resolve. It strips a leadingsrc/,lib/,internal/,pkg/orvendor/segment unconditionally and with no marker, sointernal/soak/report.gobecomessoak/report.go— whichENOENTs from the directory the command ran in. No bundled filter uses it. Reach for it only when the path is display-only and will never be opened.
Custom Filters
snip init # creates ~/.config/snip/filters/
vim ~/.config/snip/filters/my-tool.yaml # add your filter
User filters take priority over built-in ones. Later directories in the list override earlier ones.
Configuration
Optional TOML config at ~/.config/snip/config.toml:
[tracking]
db_path = "~/.local/share/snip/tracking.db"
[display]
color = true
emoji = true
quiet_no_filter = false # suppress "no filter" stderr messages
summary = false # prepend a "[snip: ...]" line showing which filter/args were applied
[filters]
dir = "~/.config/snip/filters"
[filters.enable]
# git-diff = false # disable a specific built-in filter
[filters.global]
# max_output_bytes = 65536 # hard cap on the bytes any filter emits (0 = unlimited).
# Applied last, cutting on a UTF-8 rune boundary and
# appending a "... truncated at N bytes" marker that is
# counted inside the cap.
[tee]
enabled = true
mode = "failures" # "failures" | "always" | "never"
max_files = 20
max_file_size = 1048576
# project_marker = ".git" # write tee files to <repo-root>/.snip/tee/ instead
# of the global dir when the marker is found by
# walking up from the working directory; a
# .gitignore is created there so logs are never
# committed, and snip falls back to the global dir
# if the repo root is not writable
Multiple Filter Directories
filters.dir accepts a single string or an array of directories. This enables per-project filter rules alongside global ones:
[filters]
dir = [
"~/.config/snip/filters",
"${env.PWD}/.snip",
]
Later directories take priority: a filter in .snip/ overrides one with the same name in ~/.config/snip/filters/.
Environment Variable Expansion
All path values support ${env.VAR} syntax to reference environment variables:
[filters]
dir = "${env.HOME}/.config/snip/filters"
[tracking]
db_path = "${env.XDG_DATA_HOME}/snip/tracking.db"
Tilde expansion (~/) is also supported and applied after env var expansion.
Runner Prefixes
When a command is run through a runner wrapper, snip strips the wrapper, applies the inner command's filter, and leaves the wrapper in place. So uv run pytest is filtered by the pytest filter with no extra configuration -- no need to copy or duplicate the filter.
uv run pytest -v # filtered by the pytest filter
uv run --python 3.12 pytest # runner flags before the command are skipped
poetry run ruff check . # filtered by the ruff filter
Built-in prefixes: uv run, poetry run, pdm run, pipenv run, rye run, hatch run, and the shell wrappers noglob, nocorrect, command, exec. Add your own (e.g. a container or env wrapper) with transparent_prefixes:
[filters]
transparent_prefixes = ["docker exec mycontainer", "direnv exec ."]
Detection is fail-closed: the first non-flag token after the prefix must be a known snip command, otherwise the command passes through untouched. A runner executing an unknown program (e.g. uv run bash -c ...) is never rewritten and never auto-allowed, preserving snip's confirmation-prompt guarantee.
Design
- Startup < 10ms — snip intercepts every shell command; latency is critical
- Graceful degradation — if a filter fails, fall back to raw output
- Exit code preservation — always propagate the underlying tool's exit code
- Lazy regex compilation —
sync.Onceper pattern, reused across invocations - Zero CGO — pure Go SQLite driver, static binaries, trivial cross-compilation
- Goroutine concurrency — stdout/stderr captured in parallel without thread pools
Design Philosophy
snip chose a fundamentally different approach to LLM token reduction: filters are data, not code. The binary is the engine, filters are YAML data files, and the two evolve independently.
| rtk (Rust) | snip (Go) | |
|---|---|---|
| Filter authoring | Write Rust, recompile, wait for release | Write YAML, drop in a folder, done |
| Filter format | Compiled into the binary | Declarative YAML, engine and filters evolve independently |
| Custom filters | Fork the repo, add Rust code | Create a .yaml file in ~/.config/snip/filters/ |
| Concurrency | 2 OS threads | Goroutines (lightweight, no thread pool) |
| SQLite | Requires CGO + C compiler | Pure Go driver, static binary, no dependencies |
| Cross-compilation | Per-target C toolchain | GOOS=linux GOARCH=arm64 go build |
| Pipeline actions | Built-in strategies | 19 composable actions (keep, remove, regex, JSON, state machine...) |
| Contributing | Rust knowledge required | YAML knowledge sufficient |
Both tools solve the same problem: reducing AI token costs from verbose CLI output. snip's bet is that extensibility wins. When anyone can write a filter in 5 minutes without touching Go or Rust, the filter ecosystem grows faster.
Development
make build # static binary (CGO_ENABLED=0)
make test # all tests with coverage
make test-race # race detector
make lint # go vet + golangci-lint
make install # install to $GOPATH/bin
Documentation
Full documentation is available on the Wiki:
- Installation — Homebrew, Go, binaries (macOS/Linux/Windows), from source
- Integration — Claude Code, Cursor, Copilot, Gemini, Kilo Code, Antigravity, and more
- Gain Dashboard — Token savings reports and analytics
- Filters — Built-in filters, custom filters
- Filter DSL Reference — All 19 pipeline actions
- Configuration — TOML config, environment variables
- Architecture — Design decisions, internals
- Contributing — Dev setup, adding filters, conventions
Credits
Inspired by rtk (Rust Token Killer) by the rtk-ai team. rtk proved that filtering shell output before it reaches the LLM context window is a powerful idea for cutting AI coding costs. snip rebuilds the concept in Go with a focus on extensibility -- declarative YAML filters that anyone can write without touching the codebase.
License
MIT
Comments (0)
Sign in to join the discussion.
No comments yet
Be the first to share your take.