The second specimen: snip, the same anatomy in Go and YAML
TL;DR. Chapter 35 dissected RTK and extracted five design rules. This
chapter runs the identical protocol on snip (github.com/edouard-claude/snip), an independent
Go implementation of the same idea, built from source on this machine and pointed at the same
13-commit lab repo. The anatomy repeats almost bone for bone: a PreToolUse hook answering with
updatedInput, argument injection before the command runs, ceil(chars/4) into SQLite, a
500-byte tee floor, a 90-day retention sweep. The differences are where the education lives: snip
refuses to auto-allow nothing (it always stamps permissionDecision: "allow"), its filters (132 on the master
we build; the README advertises 127) are pure YAML data interpreted by one engine, its git log filter drops commit bodies that RTK
preserves (744 bytes vs 912 on the same history, and the missing bytes are the informative ones),
its ledger honestly records the injected command it really ran, and its meter is even blinder
than RTK's: the dashboard reported 0.5 percent saved and "Efficiency Low" on a command whose
true cut was 69 percent, because injection did nearly all the work before measurement began. Same
species, second data point, and now the rules from Chapter 35 stop being one tool's habits.
Contents
- Why a second dissection
- Getting the specimen on the table
- The hook, compared
- When there is no hook: the prompt-injection fallback
- The filter is a YAML file
- The lab rematch: same repo, both tools
- The guards, compared
- The stats: gathering, storage, and the report
- The meter, blinder
- What the pair proves
- Takeaways
Why a second dissection
Chapter 35 ended with five design rules pulled out of one codebase, which leaves an honest doubt: are those rules the anatomy of the species, or the habits of the specimen? The way to find out is the way biology found out: dissect a second one and see which organs repeat.
snip is the right second specimen for three reasons. It is a genuinely separate implementation (Go instead of Rust, YAML instead of Rust-plus-TOML, a different author) of the same product category: a CLI proxy that filters shell output before it reaches an AI assistant's context window, with hook integrations for Claude Code, Cursor, Copilot, Gemini CLI, and a dozen others. It is small enough to read in a sitting, about 9,600 non-test lines of Go plus 132 filter files. And it is candid about its lineage; the comment on its hook rewriter says it is "mirroring rtk's per-segment" behavior, so we are explicitly looking at one design expressed twice, which is exactly the experiment we want.
Don't be confused. "Independent implementation" does not mean "independent invention." The two tools share a design vocabulary on purpose (the source says so), the way two database engines both implement write-ahead logging. What is informative is where a second author, reimplementing from the same blueprint, made a different call. Those divergence points are the real design decisions; everything both tools do identically is probably forced by the problem itself.
Getting the specimen on the table
snip ships through Homebrew and GitHub releases, but for a dissection the binary must match the source we quote, so we build it. It is a Go module with the filters embedded into the binary at compile time; the entire embedding mechanism is five lines:
// embed.go
package snip
import "embed"
//go:embed filters/*.yaml
var EmbeddedFilters embed.FS
git clone https://github.com/edouard-claude/snip
cd snip && go build -o /tmp/snip ./cmd/snip
/tmp/snip version
snip vdev
(vdev because release binaries get their version stamped at link time; a local build is
honest about being a dev build.) Everything below runs this binary, built from commit 82b741b
of the master branch, on the same machine and the same lab repo as Chapter 35.
The hook, compared
Installation is snip init, which writes the same PreToolUse entry into ~/.claude/settings.json
that RTK uses. The hook reader is the same shape too, but the Go version is compact enough to
show its entire decision ladder:
// internal/hook/hook.go
var input hookInput
if err := json.Unmarshal(data, &input); err != nil {
return nil // malformed JSON: pass through silently
}
if input.ToolName != "Bash" {
return nil
}
...
// Commands containing a command substitution ($(...) or backticks) or a
// carriage return cannot be safely segmented or attested: the substituted
// content executes without ever being inspected. Pass through unchanged so
// Claude Code's confirmation prompt still fires (#88).
if HasUnverifiableConstruct(ti.Command) {
return nil
}
And the unattestable-construct check, which in RTK was a quote-aware lexer, is here three string scans:
// internal/hook/parse.go
func HasUnverifiableConstruct(cmd string) bool {
return strings.Contains(cmd, "$(") ||
strings.IndexByte(cmd, '`') >= 0 ||
strings.IndexByte(cmd, '\r') >= 0
}
Coarser than RTK's (a $(...) inside single quotes is harmless, and this check refuses it
anyway), but the refusal direction is the same: when in doubt, do not rewrite. Both authors
independently priced a false rewrite as more expensive than a missed saving. Now the live round
trip, same impersonation trick as Chapter 35:
printf '%s' '{"tool_name":"Bash","tool_input":{"command":"git log --stat -n 3"}}' \
| /tmp/snip hook
{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"allow","permissionDecisionReason":"snip auto-rewrite","updatedInput":{"command":"\"/private/tmp/snip\" run -- git log --stat -n 3"}}}
Same envelope, three telling differences from RTK's reply:
- The rewritten command is an absolute, quoted path,
"/private/tmp/snip" run -- git log ...(our build lives in/tmp, which macOS resolves to/private/tmp; a Homebrew install would show/opt/homebrew/bin/snip). RTK rewrites to a barertk git log ...and trusts PATH. The absolute path survives a PATH-less hook environment; the bare name survives a moved binary. Pick your failure mode. - The
run --form. RTK maps commands into its own subcommand tree (rtk git log), which means every supported tool needs a routing entry. snip funnels everything through onerunverb with a--separator, so the filter registry alone decides what is supported. permissionDecision: "allow"is unconditional. Chapter 35 showed RTK inserting that field only when the user's own permission rules already allowed the command, deferring to Claude Code's prompt otherwise. snip stampsallowon every rewrite it emits. Its guard against auto-allowing something dangerous is further upstream: it only rewrites commands whose base appears in its filter registry (read-mostly developer tools), and it refuses anything unattestable, so what remains is the set it considers safe. That is a defensible position and a genuinely different trust posture: RTK delegates the allow decision to your configuration; snip bundles it with the rewrite decision.
The non-Bash and substitution cases behave identically to RTK, and you can verify the silence the
same way (tool_name: "Read" produces no output; git log $(date +%F) produces no output). Same
experiment, same result, omitted here for space.
When there is no hook: the prompt-injection fallback
One part of snip has no RTK equivalent and earns its own section, because it maps directly onto
this book's central distinction. Claude Code and Cursor expose hook APIs, so snip integrates with
them structurally: the rewrite happens in the harness, and the model cannot opt out. But
Copilot, Gemini CLI, Codex, Windsurf, and Cline (in their file-based configurations) offer no
hook, so snip init --agent copilot falls back to writing an instructions file
(.github/copilot-instructions.md, GEMINI.md, AGENTS.md, and so on) that asks the model to
prefix its shell commands with snip.
That is the difference between a guarantee and a request. The hook path is the injection channel of Chapter 28: deterministic, invisible to the model, enforced at the choke point. The instructions-file path is prompt engineering: it works exactly as often as the model remembers and complies, which Chapter 20 measured to be "less often than you hope" for conventions that live only in the prompt. snip's README is upfront about the mechanism difference, and its integration table is a tidy census of which agents give tool builders a real choke point and which only give them a suggestion box. When you evaluate any context tool, this is a question to ask before any benchmark: is its integration structural or behavioral? The same binary can be a guarantee on one agent and a hope on another.
The filter is a YAML file
Where RTK's high-traffic filters are handwritten Rust (its git.rs alone outweighs snip's whole
engine) with a TOML engine for the long tail, snip goes all in on data: every one of its 132
filters is YAML, and the Go engine is a single interpreter for all of them. Here is the entire
git log filter, the counterpart of the ~90-line Rust run_log from
Chapter 35:
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"
skip_if_present:
["--merges", "--format", "--pretty", "--oneline", "-n", "--max-count"]
pipeline:
- action: "keep_lines"
pattern: "\\S"
- action: "truncate_lines"
max: 80
ellipsis: "..."
- action: "format_template"
template: "{{.count}} commits:\n{{.lines}}"
on_error: "passthrough"
Read it against the Rust version and the same three-part anatomy appears: a match clause
(with exclude_flags playing the role of RTK's "did the user already choose a format" checks), an
inject clause that shapes the command before it runs (--pretty, --no-merges, a default
-n 10, with skip_if_present encoding the politeness protocol declaratively), and a
pipeline of post-filter actions. Stage 1 and stage 2, exactly as in Chapter 35, but as
fifteen lines of configuration instead of a hundred of code. The on_error: "passthrough"
line is the whole error policy: a filter that breaks degrades to raw output rather than blocking
the agent.
The engineering trade is explicit. RTK's Rust filters can do things no declarative pipeline can
(stateful parsing, cross-line reasoning, custom summaries like "10 passed, 0 failed"), and snip
reserves that power for its action vocabulary; a YAML filter can only compose the verbs the
engine ships (keep_lines, strip_lines, truncate_lines, head, tail, format_template,
and friends). In exchange, snip's marginal filter costs a text file, its filters can be reviewed
by people who do not read Rust or Go, users can drop overrides into ~/.config/snip/filters/,
and the project's own contribution history shows the payoff: the newest filter on the master
we built (markdownlint-cli2) landed as pure YAML, no engine change required.
One difference in that YAML deserves a spotlight before the rematch: snip's format string has no
%b. RTK's had one, with a comment explaining that commit bodies carry BREAKING CHANGE notes
and design rationale worth keeping. Remember that; it is about to cost snip some signal.
The lab rematch: same repo, both tools
Same 13-commit repository from Chapter 35 (12 one-line commits, plus one
with a long subject, a three-paragraph body, and two sign-off trailers). Raw git log is 2,387
bytes. snip's turn:
/tmp/snip git log
10 commits:
29b02ab fix: stop the tokenizer from splitting emoji into surrogate halves (1...
ce2eaf5 feat: add feature 12 to the parser module (11 minutes ago) <Ada>
b836d16 feat: add feature 11 to the parser module (11 minutes ago) <Ada>
caadde4 feat: add feature 10 to the parser module (11 minutes ago) <Ada>
879920b feat: add feature 9 to the parser module (11 minutes ago) <Ada>
441dc8d feat: add feature 8 to the parser module (11 minutes ago) <Ada>
3c4f6e8 feat: add feature 7 to the parser module (11 minutes ago) <Ada>
eac4c13 feat: add feature 6 to the parser module (11 minutes ago) <Ada>
bdaa746 feat: add feature 5 to the parser module (11 minutes ago) <Ada>
3ad3d29 feat: add feature 4 to the parser module (11 minutes ago) <Ada>
Every YAML clause is visible, as every Rust rule was in Chapter 35: the injected format and
-n 10 cap, the 80-column truncation with the configured ... ellipsis, the
format_template header announcing "10 commits:". The scoreboard on identical input:
| Output | Bytes | Tokens (ceil/4) | Kept the commit body? |
|---|---|---|---|
raw git log | 2,387 | 597 | yes, plus trailers |
rtk git log | 912 | 228 | 3 lines, trailers stripped |
/tmp/snip git log | 744 | 186 | no |
snip wins the byte count by dropping bodies entirely, and that is not a win. The fix commit's body ("indexed by UTF-16 code unit... switches the whole pipeline to char indices") is the single most informative text in this repository's history, precisely the thing an agent asked "what changed recently?" needs. RTK spent 168 extra bytes keeping it. This is Chapter 27's Headroom lesson at miniature scale, now demonstrated between two tools of the same species: a compression ratio can only be ranked after a loss inspection. By bytes, snip beats RTK here; by signal per byte, it is the other way around; and neither ordering is visible from the dashboards.
The guards, compared
Chapter 35 found two safety nets in RTK: the never_worse token-count guard and the tee escrow.
snip has organs in both sockets, each mutated. Its output guard checks emptiness, not size:
// internal/engine/pipeline.go
// Safety net: a filter that strips every line would send empty output to
// the LLM, which is worse than the raw result and triggers wasteful retry
// loops (issue #85). Fall back to raw unless the input was itself empty.
func shouldRestoreRaw(filtered, raw string) bool {
return strings.TrimSpace(filtered) == "" && strings.TrimSpace(raw) != ""
}
The comment is a field report from production: an over-aggressive filter that returns nothing
makes the agent retry, and retries cost more than the filter ever saved. But note what this
guard does not do: nothing stops a snip filter from emitting more than raw (the format_template
header makes tiny outputs slightly bigger, which we are about to see in the ledger). RTK guards
the size floor, snip guards the emptiness floor, and a filter you write yourself should probably
guard both; each author's scar tissue encodes the failure they actually met.
The tee escrow is nearly a genetic copy: same 500-byte minimum, same 20-file rotation, same env
override (SNIP_TEE_DIR for RTK_TEE_DIR). One default differs: snip's out-of-the-box tee mode
is failures (escrow only when the command exits nonzero), where RTK tees large filtered
successes too, which is how its grep left us a recovery pointer in Chapter 35. Same organ,
different appetite.
The stats: gathering, storage, and the report
The measurement pipeline will feel familiar, which is the point of a second dissection. The estimator is the same four-lines-of-heuristic, translated:
// internal/utils/utils.go
// EstimateTokens estimates token count using ~4 chars/token heuristic.
func EstimateTokens(s string) int {
n := len(s)
if n == 0 {
return 0
}
return int(math.Ceil(float64(n) / 4.0))
}
Gathering happens at the end of the engine's run, and the Go source makes the measurement point
even easier to see than the Rust did. pipelineInput is the captured output of the command
with injected arguments already applied:
// internal/engine/pipeline.go
inputTokens := utils.EstimateTokens(pipelineInput)
if inputTokens > 0 {
originalCmd := command + " " + strings.Join(fullArgs, " ")
snipCmd := command + " " + strings.Join(finalArgs, " ")
outputTokens := utils.EstimateTokens(filtered)
if err := timed.Track(originalCmd, snipCmd, inputTokens, outputTokens); err != nil && ...
Storage is SQLite at ~/.local/share/snip/tracking.db (snip uses the XDG path even on macOS;
snip config prints it, along with every other resolved setting). The writer opens the database
in WAL mode with a five-second busy timeout so concurrent hook invocations do not trip over each
other, and, like RTK, it prunes on every insert; the retention policy ships inside the insert
path as a second statement:
// internal/tracking/schema.go
const cleanupSQL = `DELETE FROM commands WHERE timestamp < datetime('now', '-90 days');`
Ninety days, the same window RTK chose. The commands table is RTK's minus the project_path
column (snip's reports are always global; RTK can scope gain to the current repo), and snip
adds a table RTK does not have:
// internal/tracking/schema.go
// unfiltered_commands records commands that ran with no matching filter at all,
// used to surface filter-coverage gaps (issue #96). Only the command name and
// invocation are stored — the passthrough output is streamed straight to the
// terminal (never intercepted), so output size is not captured.
const createUnfilteredTableSQL = `
CREATE TABLE IF NOT EXISTS unfiltered_commands (
id INTEGER PRIMARY KEY AUTOINCREMENT,
timestamp DATETIME DEFAULT (datetime('now')),
command TEXT NOT NULL,
full_cmd TEXT NOT NULL
);
`
That is a coverage-gap instrument: every command snip saw but had no filter for, kept 14 days,
so snip discover can tell you which of your habitual commands are leaking unfiltered tokens
and would repay a new YAML file. It measures the tool's own blind spots, which is a discipline
Chapter 30 would endorse. Now the ledger rows our lab run
actually produced:
sqlite3 -header -column ~/.local/share/snip/tracking.db \
"SELECT original_cmd, snip_cmd, input_tokens, output_tokens, saved_tokens,
ROUND(savings_pct,1) AS pct
FROM commands ORDER BY id DESC LIMIT 2;"
original_cmd snip_cmd input_tokens output_tokens saved_tokens pct
------------ ----------------------------------------------------------- ------------ ------------- ------------ ---
git log git log --pretty=format:%h %s (%ar) <%an> --no-merges -n 10 187 186 1 0.5
git log git log --pretty=format:%h %s (%ar) <%an> --no-merges -n 10 187 186 1 0.5
Notice the snip_cmd column. RTK's equivalent stores the string rtk git log; snip stores the
actual injected invocation it executed, format string and all. For anyone auditing the ledger
later, that is the more honest record: the row itself tells you the measurement baseline was the
shaped command, no source-reading required. Small design decision, real forensic value.
The meter, blinder
And now read the numbers in that row, because they are this chapter's version of Chapter 35's discovery, sharpened. Input 187 tokens, output 186. One token saved. 0.5 percent.
The reconciliation is the same arithmetic as before. Raw git log on this repo is 597 tokens.
The injected invocation (which the snip_cmd column conveniently spells out) produces about 748
characters on this history, 187 tokens. The YAML pipeline then barely touches it: keep_lines
finds no blank lines to remove, truncate_lines shortens one long header, and format_template
adds a header line back. Net: one token. The pipeline was 597 → 187 → 186, and the meter
watches only the second arrow.
So the dashboard, on a session consisting entirely of a command it cut by 69 percent end to end, reports this:
/tmp/snip gain
snip — Token Savings Report
══════════════════════════════
Commands filtered 2
Tokens saved 2
Avg savings 0.5%
Efficiency Low
Total time 0.0s
░░░░░░░░░░░░░░░░░░░░ 1%
Top commands by tokens saved
Command Runs Saved Savings Impact
─────── ──── ───── ─────── ────────────
git log 2 2 0.5% ████████████
"Efficiency Low," says the tool, about its own best work. RTK's meter had the same blindness at 17.4-percent-versus-62; snip's shows 0.5-versus-69 because its stage 1 does an even larger share of the total cut (bodies included, remember, stage 1 dropped them). Two implementations, two authors, same architecture, same measurement artifact. That settles the question Chapter 35 left open: the stage-2-only meter is not one tool's bug. It is what naturally happens when the instrumentation point sits after the cheapest, biggest lever, and any injection-first filter you build or adopt will underreport in exactly this way unless you deliberately measure against the un-injected command. Both meters at least err conservative: real savings are never less than reported. But if you ever A/B two such tools by their own dashboards, you will be comparing their post-filters while ignoring the stage where each does its real work, and, as the rematch table showed, possibly crowning the one that discarded the most signal.
Don't be confused.
snip gainandrtk gainnumbers are not comparable, even on identical workloads. Each meters its own stage 2 against its own stage 1 baseline, and those baselines differ (snip's injectedgit logoutput is smaller than RTK's, because RTK's format keeps bodies). A cross-tool comparison needs a shared, un-injected baseline, which is precisely the protocol Chapter 27 built.
What the pair proves
Lay the two dissections side by side and the anatomy sorts itself into what the problem forces and what the author chooses.
| Organ | RTK (Rust) | snip (Go) | Verdict |
|---|---|---|---|
| Integration | PreToolUse hook, updatedInput | same, plus prompt-injection fallback for hookless agents | forced by the harness |
| Unattestable commands | quote-aware lexer refusal | three-scan string refusal | forced; rigor varies |
| Stage 1 | argument injection in code | inject: clause in YAML | forced; language varies |
| Stage 2 | per-command code + TOML engine | one engine, 132 YAML filters | genuine philosophy fork |
| Output guard | never bigger (token count) | never empty | chosen; each guards a real failure |
| Escrow | tee, 500-byte floor, 20 files | tee, 500-byte floor, 20 files | convergent down to the constants |
| Estimator | ceil(chars/4) | ceil(chars/4) | the universal donor |
| Ledger | SQLite, 90-day self-prune, project-scoped | SQLite, 90-day self-prune, global + coverage-gap table | forced shape, chosen extras |
| Meter placement | after injection | after injection | the shared blind spot |
| Permission stance | auto-allow only per user rules | auto-allow all rewrites | the real trust divergence |
The five rules from Chapter 35 survive contact with the second specimen, and the comparison adds two more worth writing down. Six: decide your permission posture explicitly, because "who gets to auto-approve the rewritten command" is a security decision that two reasonable authors resolved in opposite directions. Seven: if your integration is prompt-injected rather than hooked, label it a request, not a guarantee, and measure its compliance rate before trusting its savings math.
Takeaways
- snip is the same choke-point design as RTK, independently expressed in Go: hook → attestability screen → argument injection → pipeline post-filter → guard → tee → SQLite ledger. The repetition is the evidence: that sequence is the anatomy of the species, not a quirk.
- Filters-as-data is snip's real thesis: 132 YAML files, one interpreter,
on_error: passthroughas the universal failure policy. The trade is expressiveness (no custom summaries) for reviewability and a near-zero marginal cost per new filter. - On the same lab history, snip emitted 744 bytes to RTK's 912, and the 168-byte difference was the commit bodies, the most informative text in the repo. Byte counts cannot rank filters; loss inspection can.
- Stats are gathered by one
Trackcall per filtered command into~/.local/share/snip/tracking.db(WAL mode, 90-day self-prune at insert, no content stored), with a separate 14-dayunfiltered_commandstable that inventories the tool's own coverage gaps. Thesnip_cmdcolumn records the real injected invocation, an audit-friendly touch RTK lacks. - The meter blindness generalized: snip's dashboard reported 0.5 percent ("Efficiency Low") on a 69 percent end-to-end cut, the same stage-2-only artifact as RTK's 17.4-versus-62, now confirmed as a property of the architecture rather than one codebase. Never compare such tools by their own dashboards; use a shared un-injected baseline.
- Where agents expose no hook, snip falls back to instruction files that ask the model to prefix commands: a behavioral integration, not a structural one, and the difference is the whole subject of this book in one deployment table.
👉 Two specimens, one anatomy, seven rules. The final page collects every project, the papers behind the techniques, and the glossary, as a reference you can return to.