This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
snip is a CLI proxy written in Go that reduces LLM token consumption by 60-90% by filtering shell output before it reaches the LLM context. Inspired by rtk (Rust Token Killer), snip improves on the concept with a declarative filter DSL — filters are YAML config files, not compiled code.
The binary (snip) is the engine. Filters are data files. The two evolve independently. Anyone can contribute a filter without knowing Go.
cmd/snip/main.go # Entry point
embed.go # Embedded default filters (go:embed)
filters/*.yaml # Declarative filter definitions (132 filters)
internal/
cli/ # CLI routing, flag parsing
config/ # TOML config loading (~/.config/snip/config.toml)
display/ # Lipgloss terminal styling, gain report
engine/ # Command execution (goroutines), pipeline orchestration
filter/ # DSL types, 20 built-in actions, YAML parser, registry
hook/ # Claude Code PreToolUse hook handler (native Go, no bash/jq)
initcmd/ # Multi-agent hook installation (Claude Code, Cursor, Codex, Windsurf, Cline)
discover/ # Session history scanner for missed savings
learn/ # Scan session history for failed commands, generate CLI-correction rules
verify/ # Run the inline `tests:` blocks of filter YAML files
trust/ # SHA-256 trust store for user filter files (trust/untrust)
economics/ # Model pricing tiers, $ savings estimates (cc-economics)
hookaudit/ # Audit log of hook rewrite decisions (hook-audit)
inspect/ # Code quality checks on snip's own Go source
tracking/ # SQLite token tracking (pure Go, no CGO)
tee/ # Raw output recovery on failure
utils/ # Truncate, StripANSI, EstimateTokens, LazyRegex
tests/fixtures/ # Test fixtures for integration tests
- Intercept command via
snip hook(Claude Code PreToolUse, native Go) - Route to matching filter (O(1) registry lookup)
- Execute original command, capture stdout/stderr via goroutines
- Apply declarative filter pipeline (regex-based: keep/remove lines, reformat, template)
- Output filtered result, track token savings in SQLite
- Static binaries, no runtime dependencies, trivial cross-compilation
- Goroutines naturally solve the stdout/stderr concurrent read problem (vs 2 OS threads in rtk)
- Lower barrier to entry for community contributions
- Pure Go SQLite driver (no CGO needed)
make build # Build static binary (CGO_ENABLED=0)
make build-lite # Build without SQLite tracking (-tags lite, ~5MB smaller)
make test # Run all tests with coverage
make test-race # Run tests with race detector
make lint # go vet + golangci-lint (version pinned in .golangci-lint-version)
make verify # Run the inline `tests:` blocks of filters/*.yaml
make vulncheck # govulncheck ./...
make ci # Pre-PR gate: test-race + verify + lint + vulncheck
make install # Install using GOBIN or the Go environment
make install-lite # Install lite variant
make upgrade # Replace active snip (GOBIN overrides)
make upgrade-lite # Replace active snip with lite variant
go test -run TestName ./internal/filter/... # Single test
goreleaser release --snapshot --clean # Test release build locally- snip is usually installed as a hook here, so it filters
goandgitoutput while you work. For raw output:/usr/bin/git ..., orprintf '#!/bin/sh\nexec go "$@"\n' > /tmp/rawgo && chmod +x /tmp/rawgo snip verifyruns the embedded filter set: rebuild the binary after editing anything infilters/.make verifygoes throughgo run, so it always sees the currentfilters/- Filter inline
tests:blocks are CI-enforced by theVerify filtersstep in.github/workflows/ci.yaml
- Startup < 10ms — snip intercepts every shell command; latency is critical
- Hook path is fast —
snip hookloads filters + registry only (no SQLite, no tracking) - SQLite init cost —
modernc.org/sqliteinit()adds ~3.4ms; useNewLazyTracker+WarmUp()to overlap with command execution - Graceful degradation — if a filter fails, fall back to raw command output
- Exit code preservation — always propagate the underlying tool's exit code
- No async runtime — goroutines are sufficient; avoid heavy dependencies
- Lazy compilation — compile regex once (sync.Once), reuse across invocations
- Minimal memory — stream and filter line-by-line, don't buffer entire output
Two build modes via Go build tags:
- Full (default): includes
modernc.org/sqlitefor token tracking viainternal/tracking/driver.go - Lite (
-tags lite): excludes SQLite, usesdriver_lite.gostub — startup ~3ms faster
Tests requiring SQLite must have //go:build !lite tag. Check tracking.DriverAvailable at runtime.
Filters are declarative YAML files with 20 built-in actions:
keep_lines, remove_lines, truncate_lines, truncate_bytes, 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
Gotchas that have caused real bugs:
aggregatereplaces the lines it counted unlessappend: true(caused #134, #136 in three filters){{.count}}is the number of lines reaching the template, not entities: wrong after any stage emitting a summary, an overflow marker or a cap (caused #125). Prefer the tool's own count over recomputing one- Any stage that drops or counts lines must sit where payload and metadata are still distinguishable —
a
remove_linesafter a state machine will happily delete panic text exclude_flagsandskip_if_presentmatch withstrings.HasPrefix;skip_if_presentis all-or-nothing, one match disables the whole injectioninject.argsmay reformat the answer, never suppress part of it. A flag that hides content (--stat,--no-merges) needs an escape inexclude_flagsand must be named in the output (#124)
Uses GoReleaser (.goreleaser.yaml) + GitHub Actions (.github/workflows/release.yaml).
A push of any v* tag triggers CI to build cross-platform binaries and create a GitHub release.
Every push that changes behavior must include a version tag:
- Patch (
v0.1.1): bug fixes, no API change - Minor (
v0.2.0): new features, backward-compatible - Major (
v1.0.0): breaking changes
git tag -a v0.1.1 -m "fix: description" && git push origin v0.1.1make testpasses- Commit with conventional prefix (
fix:,feat:,breaking:) - Create annotated tag following semver
- Push tag — CI handles release automatically
- For first-time or local validation:
goreleaser release --snapshot --clean
- All code, comments, variable names, commits, and documentation files must be in English
- Direct communication style — no hedging, state facts and solutions
- TDD workflow: write test first, implement, refactor
- Use context wrapping on errors:
fmt.Errorf("operation: %w", err) - When adding a new built-in subcommand: add it to both the
switchincli.goANDisBuiltInCommandinflags.go(the authoritative list of builtins) - When changing user-facing behavior, filters, or architecture: update the GitHub wiki (
git clone https://github.com/edouard-claude/snip.wiki.git) to stay in sync - Reviewing a fix: revert the production hunk, re-run the new test, confirm it fails. Two of six PRs this month shipped tests that passed either way
- Before calling a defect a regression, reproduce it on
mastertoo — several reported this month were pre-existing, which changes the verdict from "blocker" to "out of scope"