| name | snip-skill |
| version | 1.0.0 |
| description | snip CLI token optimizer — 126 YAML filters, escape protocol, safe config.
Auto-triggers when: snip is mentioned, token optimization is discussed,
CLI output is truncated, filters are being configured, or token savings
are needed. Use when: setting up snip, debugging filter issues, disabling
dangerous filters, configuring recommended settings, or understanding
snip's pipeline actions.
Keywords: snip, token, filter, CLI proxy, output truncated, token savings,
opencode-snip, config.toml, YAML filter, escape protocol, snip proxy,
token killer, tee output, loop, for loop, while loop, complex command,
compound command, shell keyword, unproxyable, corporate, team, project config,
.snip, team deployment, override filter, global limits, bypass
|
| compatibility | claude-code opencode cursor copilot |
| license | MIT |
| allowed-tools | ["Read","Write","Edit","Grep","Glob","Bash"] |
snip: Save Tokens Without Breaking Your Agent
snip is a CLI proxy that sits between your AI coding assistant and the shell.
It filters verbose command output — turning 689 tokens of go test into
"10 passed, 0 failed" (16 tokens). 60-90% token savings.
But there's a catch: filters work silently. If a filter is too aggressive,
your agent gets incomplete information and doesn't know it missed something.
One wrong filter can turn a debugging session into a guessing game.
This guide shows you exactly which filters are safe, which are dangerous,
and — most importantly — how to escape at any moment.
Before You Start: The Escape Protocol
Think of this like a plane safety briefing. You don't need it until you do.
┌─────────────────────────────────────────────────────────────────┐
│ │
│ OUTPUT LOOKS WRONG? │
│ │
│ Step 1 — Bypass one command: │
│ snip proxy <same command> │
│ Example: snip proxy curl http://localhost:8585/health │
│ Forces raw passthrough for that single command. │
│ │
│ Step 2 — See what snip hid: │
│ ls ~/.local/share/snip/tee/ │
│ Every filtered command's raw output is saved to disk. │
│ Find the latest file and cat it. │
│ │
│ Step 3 — Disable the bad filter permanently: │
│ Edit ~/.config/snip/config.toml │
│ [filters.enable] │
│ curl = false │
│ No restart needed — next command uses the new config. │
│ │
│ Step 4 — Nuke from orbit (last resort): │
│ Remove "opencode-snip@latest" from opencode.json plugins. │
│ Close terminal, reopen. snip is gone. │
│ │
│ SAFETY NET: Every filter has on_error: passthrough. │
│ If a filter crashes or panics — raw output flows through. │
│ │
└─────────────────────────────────────────────────────────────────┘
Test the escape now, before you need it:
snip proxy echo "this works"
snip echo "this goes through snip"
The plugin skips cd, source, ., export, alias, unset, set,
shopt, eval, and exec automatically. Those always run raw.
How snip Filters Actually Work
A filter is a YAML file. The binary is the engine; filters are data.
They evolve independently. You can write one without touching Go.
Here's a real filter, fully annotated:
name: "git-log"
version: 1
description: "Condense git log to hash + message + author + date"
match:
command: "git"
subcommand: "log"
exclude_flags: ["--format", "--pretty", "--graph", "--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
ellipsis: "..."
- action: "format_template"
template: "{{.count}} commits:\n{{.lines}}"
on_error: "passthrough"
Three things filters can do:
| Action | Example | What it does |
|---|
| Remove noise | remove_lines, strip_ansi | Strips progress bars, ANSI colors, download logs |
| Inject args | inject.args | Rewrites your command to produce cleaner output |
| Condense output | head, truncate_lines, aggregate | Keeps first N lines, summarizes counts |
The 19 pipeline actions: keep_lines, remove_lines, truncate_lines,
strip_ansi, head, tail, group_by, dedup, json_extract,
json_schema, ndjson_stream, regex_extract, state_machine,
aggregate, format_template, compact_path, replace, match_output,
on_empty.
Every filter has at least one of these. Understanding which ones go too far
is the difference between safe savings and silent data loss.
Commands without a matching filter
If you run a command that has no filter, snip passes it through unchanged
with ~10ms overhead. Over 70% of commands hit no filter in a typical session.
Installation (3 ways)
curl -fsSL https://raw.githubusercontent.com/edouard-claude/snip/master/install.sh | sh
go install github.com/edouard-claude/snip/cmd/snip@latest
brew install edouard-claude/tap/snip
Verify:
snip --version
snip config
OpenCode plugin
Add to ~/.config/opencode/opencode.json:
{
"plugin": ["opencode-snip@latest"]
}
The plugin uses the tool.execute.before hook to prefix every bash tool
call with snip. It skips commands already starting with snip, so
snip proxy works even with the plugin active.
Other assistants
| Tool | Command |
|---|
| Claude Code | snip init |
| Cursor | snip init --agent cursor |
| Copilot | snip init --agent copilot |
| Gemini CLI | snip init --agent gemini |
The Safe Zone: Filters You Can Trust
These filters remove noise while preserving the information your agent needs.
They strip progress bars, download logs, and boilerplate — but errors,
warnings, results, and structure survive intact.
How to read this section
Each entry shows:
- What the filter removes
- What the agent still sees
- A before/after example
Git (except diff/show)
git-log:
removes: full commit metadata (author email, timestamp, parent hashes)
keeps: "hash message (ago) <author>", defaults to 10 entries
safe: ✅ You see commit history without the bloat
git-status:
removes: verbose "On branch main\nYour branch is up to date..." header
keeps: porcelain filenames + status summary like "M: 2, ??: 1"
safe: ✅ All filenames preserved, just the greeting removed
git-branch:
removes: nothing important
keeps: branch list, head 20 branches
safe: ✅ If you have >20 branches, add `-a` to skip the filter
git-push / git-pull / git-fetch / git-commit / git-stash / git-add / git-worktree:
safe: ✅ They strip remote progress noise and keep results/errors
Test runners
go-test:
inject: --json flag
removes: verbose per-test JSON
keeps: "10 passed, 0 failed"
safe: ✅ Pass/fail is the signal
cargo-test:
removes: Compiling/Downloading progress
keeps: test results (pass/fail/ignored) + failure details
safe: ✅
pytest:
inject: --tb=line -q
removes: progress dots, stack trace context lines
keeps: "FAILED file::test" + "N passed, M failed"
safe: ✅
jest / vitest:
removes: stack traces, source context, console.log
keeps: PASS/FAIL per suite + summary line
safe: ✅
Linters and type checkers
tsc:
removes: source context lines, underline markers
keeps: "file.ts:line:col - error TS2304: message"
safe: ✅ The error location + code is the signal
eslint / ruff / mypy / basedpyright / biome / oxlint:
removes: context lines, blank lines
keeps: "file:line:col error rule" + problem count
safe: ✅
shellcheck / hadolint / markdownlint / yamllint / pre-commit:
removes: blank lines, ANSI
keeps: issues by file + "ok" on clean
safe: ✅
Build tools
make:
removes: "make[N]: Entering/Leaving directory", "Nothing to be done"
keeps: ALL actual build output (commands, errors, warnings), head 50
safe: ✅ Only directory navigation noise removed
gradle / mvn:
removes: > Task, Downloading, INFO boilerplate
keeps: BUILD result, errors, warnings
safe: ✅
cargo-build / cargo-check / cargo-clippy:
removes: Compiling/Checking/Fresh/Downloading progress
keeps: errors, warnings, "Finished" line
safe: ✅
go-build / go-vet / golangci-lint:
removes: compilation progress, blank lines
keeps: errors only, "ok (compiled)" on success
safe: ✅
Package managers
npm-install:
removes: deprecation warnings, funding nags, "npm notice"
keeps: "added 245 packages" or "up to date"
safe: ✅ Errors are preserved (ERESOLVE, peer dep conflicts)
pip-install:
removes: Downloading/Using cached progress, pip version notice
keeps: "Successfully installed ..." or "ERROR: ..."
safe: ✅
pnpm-install / brew-install / composer-install / poetry-install:
removes: progress bars, download logs
keeps: result summary + errors
safe: ✅
Docker (except logs)
docker-build:
removes: sub-step sha256, transferring context timing
keeps: step names (FROM, RUN, COPY), ERROR lines, final result
safe: ✅ Build output is 90% noise, errors fit in 30 lines
docker-ps / docker-images:
removes: nothing significant
keeps: all output, head 30 entries
safe: ✅
docker-compose:
removes: Pulling/Extracting/Waiting progress
keeps: service status
safe: ✅
Infrastructure
terraform / tofu:
removes: blank lines
keeps: plan changes (+/-/~ lines), errors
safe: ✅
kubectl-get:
removes: blank lines
keeps: resources with status, head 30
safe: ✅
gh-pr / gh-issue / gh-run:
removes: blank lines
keeps: all content, head 30 entries
safe: ✅
helm / ansible-playbook / gcloud / aws:
removes: progress/WARNING noise
keeps: results, errors, status
safe: ✅
The Danger Zone: Filters to Disable
These filters are the reason this guide exists. They strip or truncate
information that your agent may need. Disable them all.
Critical: curl and wget
curl — the most dangerous filter in the set.
pipeline:
- action: "remove_lines"
pattern: "^\\s*(\\*|>|\\{|\\}) "
- action: "head"
n: 50
overflow_msg: "... response truncated"
What this breaks:
Without snip (curl http://localhost:8585/health):
{
"status": "healthy",
"llm_loaded": true,
"gpu": { "cuda": true, "vram": "3.5/6GB" }
}
With snip (curl filter active):
... response truncated
The filter removes lines starting with {. JSON objects start with {.
Your entire API response is deleted. The agent sees "truncated" — but
the actual content was removed before truncation even applied.
Similarly, > lines are HTTP request headers. Gone for debugging.
Disable it: curl = false in config.
wget — less destructive but still strips useful info.
pipeline:
- action: "keep_lines"
pattern: "(Saving|saved|Resolving|Connecting|HTTP|ERROR|failed|Downloaded|Length:)"
Keeps only these specific patterns. Any custom output (--output-document,
post-download processing, webpage content) is discarded.
Disable it: wget = false.
Critical: git-diff and git-show
These inject --stat, replacing actual code changes with file-level stats.
git-diff:
Without snip (git diff):
diff --git a/src/main.rs b/src/main.rs
index abc..def 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -10,6 +10,8 @@ fn handle_request(req: Request) -> Response {
let user = req.user();
+ if !user.is_authenticated() {
+ return Response::unauthorized();
+ }
// ...
With snip (git-diff filter active):
3 files changed, 18 insertions(+), 5 deletions(-)
You see which files changed and how many lines. You don't see
what changed. For code review, debugging, or commit analysis —
this is useless.
git-show has the same injection. git show <hash> becomes
git show --stat <hash> — file stats only, no patch content.
Workaround: Use git diff --name-only or git show -p (the -p flag
is in skip_if_present so it bypasses the filter). But you have to
remember to do this every time. Better to disable.
Disable them: git-diff = false, git-show = false.
Critical: systemctl
pipeline:
- action: "keep_lines"
pattern: "(Loaded:|Active:|Main PID:|Status:|\\u25cf|failed|running|inactive|enabled|disabled|dead)"
Keeps only 4 fields: Loaded, Active, Main PID, Status. Everything else
is stripped — including the service file contents, journal output, and
dependency information.
Example:
Without snip (systemctl status nginx):
● nginx.service - A high performance web server
Loaded: loaded (/lib/systemd/system/nginx.service; enabled; preset: enabled)
Active: failed (Result: exit-code) since Tue 2026-05-05 14:22:31 UTC; 2 days ago
Docs: man:nginx(8)
Process: 1234 ExecStartPre=/usr/sbin/nginx -t (code=exited, status=1/FAILURE)
Main PID: 1234 (code=exited, status=1/FAILURE)
CPU: 5ms
With snip (systemctl filter):
● nginx.service - A high performance web server
Loaded: loaded (/lib/systemd/system/nginx.service; enabled; preset: enabled)
Active: failed (Result: exit-code) since Tue 2026-05-05 14:22:31 UTC; 2 days ago
Main PID: 1234 (code=exited, status=1/FAILURE)
Looks similar, right? But look what's missing: the Process line.
"ExecStartPre=/usr/sbin/nginx -t (code=exited, status=1/FAILURE)" —
the actual reason the service failed. The filter kept "Main PID" but
discarded the ExecStartPre line that explains why it failed.
Disable it: systemctl = false.
High risk: search and data tools
find:
head: 50 files
grep / rg:
head: 50 matches
jq:
head: 50 lines
psql:
head: 40 rows
ps:
head: 30 processes
The head limit pattern: These filters use head with overflow messages.
The overflow message tells you data was cut — but by then, the agent has
already acted on incomplete information.
Disable them: find = false, grep = false, rg = false, jq = false,
psql = false, ps = false.
Medium risk: system commands
ssh:
head: 30 lines
ping:
keep: only statistics
df:
head: 20 lines, removes tmpfs/devtmpfs
du:
head: 30 entries
diff:
head: 60 lines
rsync:
keeps: only sent/received/speedup/error
stat:
head: 20 lines
iptables:
head: 30 rules
fail2ban:
head: 20 lines
Disable them: ssh = false, ping = false, df = false, du = false,
diff = false, rsync = false, stat = false, iptables = false,
fail2ban = false.
Medium risk: logs
docker-logs:
dedup: 30 patterns, normalizes timestamps and IPs
kubectl-logs:
dedup: 30 patterns (same as docker-logs)
These use dedup which normalizes timestamps and IPs then keeps the top N
patterns. It's useful for finding unique log lines in massive output, but
dangerous when frequency matters.
Disable them: docker-logs = false, kubectl-logs = false.
Low risk: other
sops:
head: 30 lines
ollama:
removes: pulling/verifying/writing progress
head: 20 lines
Disable them: sops = false, ollama = false.
Recommended Config: Full TOML
Copy this to ~/.config/snip/config.toml:
[display]
color = true
emoji = true
quiet_no_filter = true
[tee]
enabled = true
mode = "failures"
max_files = 20
[filters.enable]
curl = false
wget = false
find = false
grep = false
rg = false
jq = false
psql = false
ps = false
systemctl = false
df = false
du = false
stat = false
ping = false
ssh = false
rsync = false
diff = false
iptables = false
fail2ban = false
docker-logs = false
kubectl-logs = false
git-diff = false
git-show = false
sops = false
ollama = false
git-log = true
git-status = true
git-branch = true
git-commit = true
git-push = true
git-pull = true
git-fetch = true
git-stash = true
git-add = true
git-worktree = true
go-test = true
go-build = true
go-vet = true
golangci-lint = true
cargo-test = true
cargo-build = true
cargo-check = true
cargo-clippy = true
cargo-install = true
cargo-nextest = true
pytest = true
ruff = true
mypy = true
basedpyright = true
pip-install = true
poetry-install = true
jest = true
vitest = true
tsc = true
eslint = true
biome = true
oxlint = true
prettier = true
npm-install = true
npx = true
pnpm-install = true
pnpm-list = true
next-build = true
turbo = true
nx = true
docker-build = true
docker-ps = true
docker-images = true
docker-compose = true
kubectl-get = true
gh-pr = true
gh-issue = true
gh-run = true
make = true
gradle = true
mvn = true
just = true
task = true
terraform = true
tofu = true
helm = true
ansible-playbook = true
gcloud = true
aws = true
rubocop = true
rspec = true
rake = true
bundle-install = true
rails-migrate = true
rails-routes = true
dotnet-build = true
dotnet-test = true
dotnet-format = true
shellcheck = true
hadolint = true
markdownlint = true
yamllint = true
pre-commit = true
mix-compile = true
mix-format = true
jira = true
prisma = true
playwright = true
liquibase = true
quarto = true
70 filters enabled, 24 disabled. The disabled ones are disabled for a
specific, documented reason — not "just in case." Read the Danger Zone
section above to understand each one.
How to Customize
Disable a built-in filter you disagree with
[filters.enable]
pytest = false
cargo-build = false
Write a custom filter
Create a YAML file in ~/.config/snip/filters/:
name: "my-tool"
version: 1
description: "Custom filter for my-tool"
match:
command: "my-tool"
pipeline:
- action: "keep_lines"
pattern: "(error|warning|result)"
- action: "head"
n: 20
overflow_msg: "... more output"
on_error: "passthrough"
User filters override built-in ones with the same name.
Test a filter without affecting your agent
snip git log -5
git log -5
If they differ more than expected, disable or adjust the filter.
Debugging Commands
snip config
snip gain
snip gain --daily
snip gain --json
snip gain --top 10
snip discover
snip discover --since 30
Summary
| Concept | Takeaway |
|---|
| Escape | snip proxy <cmd> bypasses everything |
| Safety net | on_error: passthrough on every filter |
| Raw output | ~/.local/share/snip/tee/ saves every filtered command |
| Safe filters | Test runners, linters, build tools, package managers, git (except diff/show) |
| Dangerous filters | curl, systemctl, git-diff, git-show, find, grep, jq, psql — disable all |
| Config | Copy the recommended config above |
| Customize | Disable any filter you disagree with, or write your own |
snip saves a lot of tokens. The safe filters alone (70 of them) cover
most of the noise in a typical AI coding session. The 24 disabled filters
would cost more in lost context than they'd save in tokens.
Install it. Use the config above. And remember the escape protocol —
you'll probably never need it, but you'll be glad it's there.
Limitations: Loops & Complex Commands
snip proxies a single command at a time — it matches command and
subcommand from the first word of the shell invocation. Shell parser
keywords like for, while, until, if, and case are not binaries;
they are part of the shell's syntax. Passing them as the "command" to snip
will always fail.
The Problem
snip for ip in 192.168.100.{101..110}; do
ssh root@$ip "hostname"
done
When snip receives for as the command, bash sees it as a literal argument
rather than a loop keyword. The do keyword has no matching for to pair
with, producing a syntax error. The same applies to while, until,
if/then/else, and case/esac.
Why
Snip's unproxyableReason in internal/cli/cli.go currently lists 40+
shell builtins (cd, export, alias, etc.) but is missing shell parser
keywords: for, while, until, select, if, case, fi, esac,
then, elif, else, done.
These are not binaries in $PATH — they are understood only by the shell's
parser. Running them as external commands through snip always fails.
Upstream Issue
Filed for the snip project (edouard-claude/snip):
- Issue: #66 — reported
- Pull Request: #68 — fix + corporate config submitted (replaces closed #67)
- Missing:
for, while, until, select, if, case, and their
closing keywords in unproxyableReason
- Fix: ~6 lines added to the
switch block in internal/cli/cli.go
After the fix lands (snip version containing this patch):
- The workarounds below are no longer needed — snip will reject loops with a
clear error instead of producing a cryptic bash syntax error.
- If
snip --version returns a version newer than the fix, skip workarounds
1-4 and let snip handle it naturally.
- This skill section will be updated to mark the workarounds as deprecated.
Note: a companion design proposal for corporate/project-level .snip/config.toml
was submitted as a response to #65.
Workarounds
Workaround 1 — Put snip inside the loop body
for ip in 192.168.100.{101..110}; do
snip ssh root@$ip "systemctl status nginx" 2>&1
done
Workaround 2 — Wrap as a function
scan_hosts() {
for ip in 192.168.100.{101..110}; do
ssh root@$ip "systemctl status nginx" 2>&1
done
}
snip scan_hosts
Workaround 3 — snip proxy inside loops
for ip in 192.168.100.{101..110}; do
snip proxy ssh root@$ip "systemctl status nginx" 2>&1
done
Workaround 4 — Patch UNPROXYABLE_COMMANDS in opencode-snip plugin
If you run snip via the OpenCode plugin (opencode-snip@latest), its
UNPROXYABLE_COMMANDS list also needs these keywords:
const UNPROXYABLE_COMMANDS = new Set([
"cd", "source", ".", "export", "alias", "unset", "set", "shopt", "eval", "exec",
"for", "while", "until", "select",
"if", "case", "fi", "esac", "then", "elif", "else",
])
What the Safe Config Does
Our recommended config (config.toml with 24 disabled + 70 enabled filters)
is unaffected by this limitation. The loop issue lives in snip's CLI routing
layer (unproxyableReason), not in the filter pipeline. Even if all filters
are perfectly configured, a snip for ... invocation still breaks at the
parsing stage.
Corporate & Team Deployment
snip can be configured at the project level so every developer on a team
gets the same filter behavior — without touching their personal config.
How It Works
Team lead creates: .snip/config.toml (committed to repo)
Developer clones: git clone team-repo
Developer works: snip auto-detects .snip/config.toml → applies team settings
No per-developer: No snip trust, no .config edits, no env vars needed
.snip/config.toml — Full Reference
mode = "project"
[filters.global]
max_lines = 0
max_line_length = 0
stream_mode = "filter"
[filters.enable]
curl = false
docker-logs = false
[filters.override.ls]
head = 0
[filters.override.dotnet-test]
stream_mode = "full"
[filters.override.make]
stream_mode = "full"
[filters.override.pytest]
head = 0
[filters.override.go-test]
head = 0
[filters.override.docker-build]
head = 200
[filters.override.npm-install]
keep_lines = "(error|warn|deprecated|notice|added|removed|packages)"
[filters.bypass]
commands = ["curl", "wget", "jq", "psql"]
[tee]
enabled = true
mode = "always"
max_files = 100
max_file_size = 10485760
Trust Model
| File | Trust Required | Why |
|---|
.snip/config.toml | No — auto-loaded | Parameter overrides only (head limits, enable/disable). Cannot execute code. |
.snip/filters/*.yaml | Yes — snip trust | Custom filter YAMLs can inject args and run pipeline actions. Must be verified. |
Two Personas
| Vibe Coder | Team Developer |
|---|
| Setup | curl install.sh | sh | git clone + .snip/config.toml in repo |
| Config | None needed | Auto-detected from repo root |
| Trust | Never needed | Only for custom filter YAMLs |
| Escape | snip proxy <cmd> | Same |
| Who controls | Themselves | Team lead via committed .snip/config.toml |
When to Use Each Mode
mode = "project": Corporate/team repos. Team lead's config is authoritative.
Individual developers cannot override. Use for: security policies, CI consistency,
per-project output limits.
mode = "user" (default): Personal projects. The developer can override any
project config setting with their own ~/.config/snip/config.toml.
Design Status
This feature was proposed in response to
edouard-claude/snip#65
with a full implementation design (~180 lines of Go code in config.go,
pipeline.go, and a new projectconfig.go). Awaiting maintainer review.
Upstream Tracker
| Issue | Topic | Status |
|---|
| #66 | Loops/complex commands break snip | PR #68 submitted |
| #65 | Corporate .snip/config.toml | PR #68 submitted |
Credits
- edouard-claude — creator of snip, the CLI proxy and all 126 built-in YAML filters. Inspired by rtk.
- VincentHardouin — creator of opencode-snip, the OpenCode plugin that auto-wraps
bash tool calls.
- DeepSeek — AI model provider (deepseek-v4-pro via Anthropic-compatible API) powering the agent behind this skill's analysis and curation.
- This skill analyzes and curates their work for safe agent use — filter audit, escape protocol, and recommended config are original to this project.