compressed directory trees for llm prompts. zero dependencies, aggressively de-noised.
npx treezip ./my-projectyou paste a tree dump into an llm so it understands your codebase. two things go wrong:
- it's verbose. every file gets its own line, its own indent guides, its own copy of the extension. a 400-file repo eats thousands of tokens on pure structure.
- it's full of junk.
node_modules,DerivedData,__pycache__,.turbo, coverage reports,.DS_Store— none of it helps the model, all of it costs tokens. one xcode build folder can turn a 100-line tree into an 11,000-line one. (that's a real number — see below.)
treezip fixes both. it compresses the structure losslessly with brace notation, and it hardcodes a multi-ecosystem noise filter so build artifacts never reach the output in the first place.
real-world result on a swift app with a checked-in derived-data dir and agent worktrees lying around:
| lines | |
|---|---|
plain tree |
11,261 |
treezip |
98 |
same source files visible. 99% less noise.
npm install -g treezipor don't install at all:
npx treezip ./my-projecttree (43 lines):
my-project/
├── docs
│ ├── api-reference.md
│ ├── contributing.md
│ └── getting-started.md
├── package.json
├── public
│ └── images
│ ├── avatar.png
│ ├── background.png
│ ├── badge.png
│ ├── banner.png
│ ├── feature.png
│ ├── hero.png
│ ├── icon.png
│ ├── logo.png
│ ├── pattern.png
│ ├── placeholder.png
│ └── screenshot.png
├── README.md
├── src
│ ├── App.tsx
│ ├── components
│ │ ├── Button.tsx
│ │ ├── Card.tsx
│ │ ├── Header.tsx
│ │ ├── Modal.tsx
│ │ └── Sidebar.tsx
│ ├── hooks
│ │ ├── useApi.ts
│ │ ├── useAuth.ts
│ │ └── useTheme.ts
│ ├── index.ts
│ └── utils
│ ├── api.ts
│ ├── auth.ts
│ ├── format.ts
│ └── validate.ts
├── tests
│ ├── Button.test.tsx
│ ├── Card.test.tsx
│ ├── Header.test.tsx
│ ├── Modal.test.tsx
│ ├── setup.ts
│ └── Sidebar.test.tsx
└── tsconfig.json
treezip (12 lines):
my-project/
├── {.gitignore,README.md,package.json,tsconfig.json}
├── docs/{api-reference,contributing,getting-started}.md
├── public/images/{avatar,background,badge,banner,feature,...}.png
├── src/
│ ├── {App.tsx,index.ts}
│ ├── components/{Button,Card,Header,Modal,Sidebar}.tsx
│ ├── hooks/{useApi,useAuth,useTheme}.ts
│ └── utils/{api,auth,format,validate}.ts
└── tests/
├── setup.ts
└── {Button,Card,Header,Modal,Sidebar}.test.tsx
every brace group decompresses to exactly one path per entry — the compression is lossless (except explicit ... truncation, which is marked so the model knows more files exist).
| pattern | meaning | example |
|---|---|---|
{a,b,c}.ext |
siblings sharing an extension | {Button,Card,Modal}.tsx |
{a.x,b.y} |
siblings with mixed extensions | {README.md,tsconfig.json} |
dir/{stems}.ext |
whole dir shares one extension | utils/{api,auth,format}.ts |
dir/file |
single-child chain collapsed | scripts/build.sh |
{a,b,...}.ext |
truncated — more files exist | images/{logo,hero,...}.png |
- mixed directories (files + subdirs): all files fold into one
{...}set, subdirs listed after - files-only directories: grouped by shared extension, compound-aware —
.test.tsgroups separately from.ts,.config.jsfrom.js - inline collapse: a directory that compresses to a single item renders on one line
- single-child chains:
a/b/c.txtwhen each level has exactly one child - truncation: groups over 8 items show the first 5 +
...
every output starts with a 3-line legend:
# COMPRESSED FILE TREE — AI READ GUIDE
# {a,b,c}.ext → siblings sharing extension | scripts/f.js → collapsed single-child dir
# {...}.ext → truncated list (more exist) | decompress: expand braces × each entry = 1 path
paste the output anywhere — the model gets the decoding instructions for free, no system-prompt setup needed.
this is the part most tree tools don't do. ripgrep, fd, tokei, eza all delegate to .gitignore — which is great until the repo has no gitignore, or the junk is inside a worktree, or someone pointed -derivedDataPath at a folder inside the repo. treezip ships its own opinionated filter, built from the github/gitignore templates, vendor docs, and the default-ignore lists of repomix/gitingest. it works in three layers:
distinctive names that are noise in any repo, skipped unconditionally:
- vcs internals:
.git(dir and the worktree/submodule pointer file),.hg,.svn,.bzr,.jj,.sapling - os junk:
.DS_Store,.Spotlight-V100,.Trashes,.fseventsd,Thumbs.db,desktop.ini,$RECYCLE.BIN,__MACOSX,._*appledouble files - js/ts:
node_modules,.pnpm,.next,.nuxt,.output,.turbo,.parcel-cache,.vite,.svelte-kit,.astro,.angular,.docusaurus,.expo,.wrangler,.vercel,.netlify,.serverless,coverage,.nyc_output,storybook-static,*.tsbuildinfo,.eslintcache - python:
__pycache__,.venv,venv,.tox,.nox,.pytest_cache,.mypy_cache,.ruff_cache,.hypothesis,.ipynb_checkpoints,*.egg-info,htmlcov,*.pyc,.coverage - swift/xcode:
DerivedData,.build,.swiftpm,xcuserdata,Carthage,SourcePackages,Index.noindex,ModuleCache.noindex,Intermediates.noindex,CompilationCache.noindex,SDKStatCaches.noindex - everything else:
.gradle(jvm),zig-cache/.zig-cache/zig-out,_build/.elixir_ls(elixir),.dart_tool(flutter),CMakeFiles/_deps/cmake-build-*(cmake),.vs/.idea(ide caches),.bundle/.yardoc(ruby),.terraform/.terragrunt-cache/.vagrant/.direnv(infra)
build roots can be named anything. xcode's -derivedDataPath lets you point builds at .test-app-build/ or whatever/; python venvs get called env, myenv, env310. name-matching can't catch these — so treezip peeks one level inside every directory:
- contains
pyvenv.cfg→ it's a venv, skip it - contains
Index.noindex/ModuleCache.noindex/SDKStatCaches.noindex/CompilationCache.noindex→ it's a derived-data root, skip it
whatever it's named, however deep it hides.
generic words like target and build are dangerous to blanket-ignore — a docs folder named build/ is real content. so these only get skipped when a sibling file proves the ecosystem:
| dir | skipped only next to |
|---|---|
target/ |
Cargo.toml or pom.xml |
build/ |
build.gradle, build.gradle.kts, settings.gradle(.kts) |
deps/ |
mix.exs |
bin/, obj/ |
a .csproj/.fsproj/.vbproj/.sln |
plus parent-scoped rules where a dot-dir is half config, half cache:
.claude/worktreeshidden,.claude/skillskept.yarn/cache,.yarn/unpluggedhidden,.yarn/patches,.yarn/releaseskept
what never gets hidden
signal stays, always:
- lockfiles —
package-lock.json,pnpm-lock.yaml,yarn.lock,bun.lock,Cargo.lock,Package.resolved,poetry.lock,uv.lock .env,.envrc— you want to know those exist.vscode,.github— committed config, often load-bearing.xcodeprojbundles — project definition is source (only thexcuserdatainside is junk)
import { scan, compress, render, countNodes } from "treezip";
const tree = scan("./my-project"); // filtered TreeNode
const compressed = compress(tree); // CompressedItem[]
const { dirs, files } = countNodes(tree); // post-filter counts
const output = render("my-project/", compressed);
console.log(output);the noise predicates are exported too, if you want the filter without the tree:
import { isNoiseDir, isNoiseFile, hasNoiseMarker, NOISE_DIRS, NOISE_FILES } from "treezip";
isNoiseDir("node_modules", "my-project", []); // true
isNoiseDir("build", "repo", ["build.gradle"]); // true (gradle proven)
isNoiseDir("build", "repo", ["README.md"]); // false (could be real)
hasNoiseMarker(["pyvenv.cfg", "bin", "lib"]); // true (it's a venv)- zero dependencies. stdlib
fs+path, nothing else. install is instant, supply chain is empty. - symlinks are skipped, so cycles can't happen and the tree never leaves the root.
- deterministic output: files first, then dirs, both alphabetical — same input, same bytes out.
- the root is never filtered. if you explicitly run
treezip ./node_modules, you getnode_modules— you asked, treezip answers. - releases are automated: every push to
mainruns the test suite, bumps the patch version, tags, and publishes to npm.
mit