silence
Strips comments in an agent post-write hook. Preserves doc comments and directives by default (e.g. JSDoc, eslint-disable-next-line, @ts-check, noqa:, TODO, FIXME, HACK).
Works with Claude Code, Codex, Opencode, and Pi.
Supports TypeScript/JavaScript, Python, Rust, Go, C/C++, Java, Kotlin, C#, Swift, CSS, JSON, YAML, Astro.
Grammars are downloaded lazily to avoid a huge binary.
install
yolo curl to shell
|
install with cargo
&&
or grab a binary from Releases.
tldr
usage
commands
silence strip — remove or check for comments
<path>…— file or directory (directories recurse; omit when using a git scope flag)--check— print what would be removed; exit 1 if any--inline— remove only line comments (//,#)--block— remove only block comments (/* … */)--preserve-lines— leave blank lines where comments were--backup— write a<file>.baknext to each modified file--no-default-preserve— drop the built-in preserve list and directive detection--threads N— parallelism (default: CPU count)--verbose— verbose output--staged— only comments inside staged hunks--unstaged— only comments inside unstaged + untracked changes--changes— strip comments inside all uncommitted changes
silence hook — agent post-edit hook
[path]…— optional paths; reads the agent's stdin event when omitted- strips comments inside the lines the write added, when the agent reports them
(Claude Code's
structuredPatch); falls back to the uncommitted change otherwise. always exits 0 - those line numbers are recorded before hooks run, so if another post-write
hook rewrites the same file, run
silence hookbefore it - feeds the model a short note so it learns the comments were stripped and
stops re-adding them: Claude Code and Codex read the
additionalContextstdout JSON natively; the Opencode and Pi plugins splice it into the tool result --no-default-preserve— same as onstrip
silence hooks — install into agent configs
install— wiresilence hookinto~/.claude/,~/.codex/,~/.config/opencode/plugins/,~/.pi/agent/extensions/install --to codex --to claude— selected agents onlyinstall --project— project-local paths under the current diruninstall— remove installed hooksstatus— per-agent install state
silence config
show— print active configuration and where it came fromshow --no-default-preserve— preview preserve rules with defaults offinit— write an example.silence.tomlto the current dir
silence llm — usage guide for agents
preserve rules
A comment is kept when it matches a preserve pattern or looks like a
directive: i.e. body starting with @, shaped namespace:value, or an XML-ish
<tag … /> (so @ts-ignore, //go:embed, /// <reference /> survive
without a rule each). A #! shebang on line 1 is never removed.
Machine markers survive too, so the paired sentinels other tools write
between ({/* impeccable-variants-start cd383158 */} … {/* impeccable-variants-end cd383158 */}) are not stripped out from under them. A sentinel counts only when
its partner is in the same file and opens before it closes: -start/-begin
needs its -end/-finish, and vice versa. That is deliberate — // front-end only
and // cold-start path have a sentinel's exact shape, and only the missing
partner tells them apart from // codegen-start. A lone half is treated as prose
and goes.
The rule is suffix-anchored, so prefix forms (BEGIN … / END …) are not detected;
colon forms (codegen:start) are kept by the directive rule instead and are not
subject to the pairing requirement. A file holding both // week-start and
// week-end keeps both — over-preserving is the safe direction.
Patterns match anywhere in a comment but only as whole words, so noop keeps
// noop without keeping // snoop on the socket. A trailing s is the same
word (TODOs), a pattern's punctuated ends are unconstrained (#region,
biome-), and only an ASCII neighbour can continue a word — // TODO修复这个
is still a TODO. A pattern written in lower case matches any casing (noop
covers NOOP); one written with a capital must match as written, so HACK
stays a marker while // half-baked hack. stays prose.
In JSX, braces holding nothing but comments go when the last of those comments
goes and removing them cannot change what the markup renders — a bare {}
left in the markup is worse than the comment was, but changing the rendered
text is worse still. An expression container splits the text around it, and a
line break renders as a space inside a run of text and as nothing at its edge,
so the braces sometimes hold a space apart. Where they do, a {} stays: that
is the tool saying it checked. A surviving comment keeps them too, since it
still has to live somewhere.
.silence.toml (searched cwd → git root → ~/.config/.silence.toml):
= ["TODO", "FIXME", "*IMPORTANT*"] # extra patterns; globs allowed
# use_default_preserve = false # drop built-ins (default: true)
--no-default-preserve drops the built-ins and directive detection but keeps
your preserve list. .silenceignore (same format as .gitignore, optionally
at ~/.config/.silenceignore) excludes files from walks.
example usage
rant
Yes, machine, you have obeyed. Yes, the code is blazing fast, just like I asked. Yes it's "no-slop and production-grade". Neither the code reviewer, nor future me, nor future you spending tokens reading this code needs to know that I asked.
contributing
design
Tree-sitter parses each file to a CST; a tiny (comment) @comment query
returns exact byte spans; spans matching preserve patterns are dropped (and,
in git mode, spans outside changed line ranges); the file is reassembled.
The engine (silence-core) is I/O-free so the logic is
unit-tested in isolation and u can build on top of it.
build
Needs a Rust toolchain (1.85+). git2 links libgit2 (vendored by default
via the crate; system cmake/C toolchain may be required on first build).
cargo build --release
cargo test
./target/release/silence --help
hook benchmark
Measures end-to-end silence hook latency (process spawn + git scan + parse).
Build release first, then:
./scripts/bench-hook.sh
Env: RUNS (default 50), WARMUP (default 5), SILENCE_BIN (path to binary).
adding a language
Built-in: TypeScript/JavaScript, Python. Everything else (Rust, Go, C/C++, TOML, …)
downloads on first use into ~/.config/silence/grammars/ from GitHub release assets.
- add optional pack metadata in
silence-langs(grammar_pack_id, extensions) - add the pack to
silence-grammar-packs/build.rsand release assets in.github/workflows/release.yml - wire
silence-grammarsensure();every_grammar_loads_and_query_compilesinsilence-strip-grammarsverifies query/ABI
Some files embed a second language. Astro's frontmatter is TypeScript, but the Astro
grammar returns it as one opaque node. Lang::injections() lists these sub-language
regions; silence re-parses each with the inner grammar and merges the comments back.