Strips executable lines from your code to prepare tasty context for your agent.
Walks a directory (respecting .gitignore), parses each supported source file
with ast-grep, and elides function bodies,
constant values, and long string literals — keeping declarations, signatures,
and types intact. The result prints as a single Markdown document, ready to
paste into an LLM context window.
// before
pub fn checkout(cart: &Cart, user: &User) -> Result<Receipt> {
let total = cart.total();
charge(user, total)?;
Ok(Receipt::new(total))
}// after — comments/imports gone, body elided, signature kept
pub fn checkout(cart: &Cart, user: &User) -> Result<Receipt> {}Each language is driven by a YAML rule file matched against the AST, not by
hardcoded per-language logic. Built-in support: Rust, PHP, TypeScript, TSX,
JavaScript, Python, Go, Java, C#, Ruby, C++, C, Kotlin, Swift, Scala, Bash, Lua,
Dart, Elixir, Haskell, Nix, Solidity, JSON, YAML, HTML, CSS, and HCL — every
grammar ast-grep bundles except Markdown (prose, nothing to strip). You can
add a language with a dynamic tree-sitter grammar, and extend or override any
language's rules from contasty.toml — both
without rebuilding contasty.
cargo install contastyDownload a pre-built binary from the latest release.
contasty src/ > context.md # strip a directory
contasty src/lib.rs # strip a single file
contasty src/ tests/ > context.md # multiple paths (deduped union)
contasty src/lib.rs src/main.rs # several files at once
contasty 'src/**/*.rs' # glob (quote it; expanded internally)
contasty 'crates/*/src' # glob to dirs; each subtree is walked
contasty # default path is "."
contasty --strip=comments,imports src/ # strip comments and imports
contasty --strip=tests src/ # also strip test functions
contasty --strip=all src/ # strip everything (alias: everything)
contasty --strip=all,!body src/ # strip all except bodies
contasty --strip=none src/ # strip nothing (keep all categories)
contasty --strip=none tests/ # per-path: keep everything in tests/
contasty --format=json src/ # emit a JSON bundle instead of Markdown
contasty --stats src/ # print compactization statistics
contasty --config path.toml src/ # use a specific contasty.toml
contasty --no-reformat src/ # skip all post-strip reformatting
contasty --ignore=disable src/ # include .gitignored files too
contasty --ignore=reverse src/ # only .gitignored files
contasty A --ignore=disable B --ignore=enable C # per-path mode switchingMultiple arguments resolve to a deduped, sorted union of source files. A folder is
walked .gitignore-aware; a glob is expanded internally (quote it) and a glob over
directories walks each subtree. A glob matching nothing warns; a missing path errors.
Output defaults to Markdown. Pass --format=json for a pretty-printed JSON bundle
shaped as { "base": <dir>, "files": [{ "path", "lang", "content" }] }.
Four categories control what is stripped:
| Category | Default | Example |
|---|---|---|
comments |
stripped | --strip=comments |
imports |
stripped | --strip=imports |
tests |
kept | --strip=tests |
body |
stripped | --strip=body |
--strip is repeatable, interleaved with paths (find-style): each occurrence sets
the strip set for the paths that follow. Comma-separated; prefix a category with !
to remove it. all (alias everything) strips all four; none strips nothing.
--ignore=<mode> controls .gitignore filtering and is repeatable, interleaved
with paths (find-style). Each occurrence sets the mode for the paths that follow:
| Mode | Effect |
|---|---|
enable |
Respect .gitignore — only non-ignored (default) |
disable |
Include ignored files too (everything) |
reverse |
Only .gitignored files |
The default (before any --ignore) is enable. Query files can set their own mode
with the ignore: field (see docs/queries.md).
Category gating applies to every supported language — test and import rules in each built-in (or custom) rule file declare which category gates them, so the same flags work uniformly.
--stats prints original-vs-compacted line counts (code / comments / blanks) and
an approximate token figure (~tokens). That figure is a dependency-free estimate
(~bytes / 4), not a model tokenizer count — use it for relative comparison only.
Two architectures. contasty is a one-shot stripper: walk the tree, elide bodies
in place, print one document. The same-shape peer is repomix --compress. Each
is stronger at different things.
| contasty | repomix --compress | |
|---|---|---|
| Languages with body elision | 18 of 27 built-ins | 16, incl. Vue |
| Add a language without a rebuild | yes — dynamic grammar + rules | no |
| Extend / override strip rules | yes — contasty.toml | no (fixed queries) |
| Gate comments (keep / drop) | yes — per-language toggle | yes — removeComments |
| Gate imports (keep / drop) | yes — --strip=imports | no (imports kept) |
| Gate tests (keep / drop) | yes — --strip=tests | no |
| Stripped-region output | valid empty bodies, reparseable | ⋮---- placeholder markers |
| Optional reformat of result | yes — Topiary / shell-out | no |
| Runtime | single static binary | Node.js |
| Output formats | Markdown, JSON | XML, Markdown, JSON, plain |
| Token counting | no (by design) | yes, multi-tokenizer |
| Secret scanning | no | yes |
| Git integration (diffs, history) | no | yes |
| Remote repos (clone by URL) | no (local only) | yes |
| MCP server | no (CLI; agents shell out) | yes |
ctx also extracts signatures, but only for PHP. For interactive, query-on-demand context, see aider's repo map or jCodeMunch-MCP — a different approach: an index the agent queries live, not a static document.
Drop a contasty.toml in your project root to tune compaction thresholds,
set default category inclusion, register dynamic grammars, and extend or
override per-language rules. All fields are optional. See
docs/languages.md and docs/custom-rules.md.
Category stripping can be set cross-language under [strip] and refined
per language under [languages.<lang>]:
strip = ["comments", "imports", "body"] # cross-language defaults
[languages.rust]
strip = ["comments", "body"] # keep imports for Rust onlyCLI --strip overrides config for all languages. Config loads first; CLI wins.
Optional per-language post-strip reformatting (cosmetic, off by default) is
configured with the reformat key — embedded Topiary or a shell-out command.
See docs/reformatting.md.
contasty matches AST nodes with ast-grep rules, so a language is data, not code.
- Built-in: drop a rule file at
src/lang/rules/<lang>.yml(embedded at build time) and register the language inRegistry::new. No per-language matching logic in Rust. - Dynamic grammar: for a language ast-grep does not bundle, supply a compiled
native tree-sitter grammar (
.so) plus a rule file and register it under[languages.<lang>]with alibraryPathincontasty.toml— no rebuild. - Extend / override: point an existing language at a user rule file with the
extend/overridekey of its[languages.<lang>]entry to append to (extend) or replace (override) its embedded rules.
The rule file format, dynamic .so grammars, JSON Schema, and editor
integration are documented in docs/languages.md; rule
extend/override in docs/custom-rules.md.
Prerequisites: Nix with flakes enabled.
direnv allow # or: nix develop
just outdatty-update # one-time: create outdatty.lock, then commit it
just check # fmt + clippy + tests + file-size + drift check
just build
just test
just cover # code coverage (70% minimum)
just fmt # format codeoutdatty gates files that must stay in
sync (see outdatty.yaml): just check fails when a source changes
but its dependents were not re-confirmed. Update them, then just outdatty-update.
See CONTRIBUTING.md for coding conventions.
MIT