CopyTree CLI
Install the CopyTree CLI with npm and use it from any terminal: the default copy, the read-only plan, inspect and explain commands, selection rules, budgets, formats, profiles, and how it lines up with Daintree.
When You Need the CLI
CopyTree started as a standalone command-line tool and is now part of Daintree. It is the same engine either way: Daintree embeds CopyTree as a library, so Copy context in the app and copytree in a terminal walk the same tree, apply the same ignore rules, and produce the same bundle for the same settings.
That means there is nothing to install to use CopyTree inside Daintree. Install the CLI when you want those bundles somewhere the app is not:
- A bare terminal on a server you SSH into.
- A CI job that has to fail when the context it was going to send is truncated or leaks a secret.
- A shell script, a Makefile, or a pipe into
jq. - An editor that is not Daintree.
CopyTree is MIT-licensed and lives at github.com/gregpriday/copytree. Installing or removing it changes nothing about the app: Daintree links the library into its own process rather than shelling out to whatever copytree happens to be on your PATH.
Install
CopyTree needs Node.js 22.12 or newer and is ESM-only. Check with node --version before you start.
npm install -g copytree@next next tag is the 1.0 release candidate, and every flag on this page is a 1.0 flag. A plain npm install -g copytree still installs the 0.17 line, which is what Daintree itself embeds today; its flag names differ in places (--only-tree rather than --format tree, --stream rather than --stdout, -f rather than --include). Run copytree --version if you are not sure which one you have.Verify the install end to end. doctor checks the binary, the clipboard integration, Git, and the policies your configuration actually resolves to:
copytree --version
copytree doctor To pin CopyTree to one repository instead of installing it globally:
npm install --save-dev copytree@next
npx copytree Shell completion is generated from the same command schema as the parser, for bash, zsh, fish, or powershell. Send it wherever your shell reads completions from:
copytree completion zsh Your First Copy
Run copytree in a project root and it selects the files, bundles them as XML, writes the bundle to a temporary file, and copies a reference to that file to your clipboard. Pasting into an agent then hands over a file to read rather than tens of megabytes of inline context.
cd ~/Projects/my-app
copytree The run reports what it did: file count, total size, and a token estimate. Read the token estimate before you paste anywhere — it is the number that decides whether the bundle is usable.
A run has exactly one destination, and naming two is an error rather than a silent precedence rule:
| Option | Where the bundle goes |
|---|---|
(none), or --reference | A temporary file, with a file reference on the clipboard. The default. |
-y, --clipboard | The clipboard, as text. |
--stdout | Standard output, for a pipe or a redirect. |
-o, --output <file> | A file you name. |
The path argument is optional and takes a directory, a single file, or a GitHub URL:
copytree src
copytree https://github.com/daintreehq/daintree
copytree https://github.com/daintreehq/daintree/tree/main/src -o daintree-src.xml Temporary bundles land in <temp>/copytree/<project>/ and are reclaimed by copytree cache gc on a seven-day retention policy, so a reference you copied last month will not still resolve.
Look Before You Copy
Four read-only commands answer the questions a copy cannot. None of them read file contents, write an export, or touch the clipboard, so they are safe to run against anything.
| Command | What it answers |
|---|---|
copytree plan . | Exactly which files, in what order, at what size, and which budget binds first. |
copytree inspect . | Structure, the active rules, the effective profile, and the budgets in force. |
copytree explain docs/README.md | Why one path is in or out, down to the rule, its source file, and its line. |
copytree ignore context . | A content-free inventory to write a .copytreeignore against. |
plan is the one to reach for first. It selects the same files, in the same order, under the same budgets as the real run, and --explain adds the excluded entries with the reason each was dropped:
copytree plan .
copytree plan . --explain
copytree plan . --summary
copytree plan . --format json -o plan.json inspect takes a --view: summary, tree, extensions, rules, profile, budgets, or all. copytree inspect . --view profile is the fastest answer to "which profile is this run actually using, and where did it come from".
Choosing What Goes In
Selection options are repeatable, one value per occurrence: --include a --include b, not a comma-separated list.
| Option | Effect |
|---|---|
--include <glob> | Narrow the selection. A file has to match at least one. |
-x, --exclude <pattern> | Add a Git-style exclusion rule. |
--scope <path> | Traverse only this literal path, with the whole repository's rules still applied inside it. |
--force-include <glob> | Override ordinary excludes and the per-file size gate. Budgets still apply. |
--ext <extension> | Keep only this extension (ts or .ts). |
--max-depth <n> | Limit traversal depth from the project root. |
-m, --modified | Working-tree changes only. |
--staged | Staged changes only. |
-c, --changed <ref> | Files changed since a Git reference, such as main or HEAD~1. |
Those three Git modes are mutually exclusive, in the same way the destinations are.
--include matches file paths, so a folder needs a glob: pass src/panels/**, not src/panels. --scope is the exact opposite — literal paths, so pass src/panels there and skip the glob escaping entirely. This is the same distinction the app draws between filter and scopePaths.Ignore files
CopyTree runs one ignore engine over nested .gitignore files at every depth, .git/info/exclude, your global gitignore, .copytreeignore, and .copytreeinclude. There is no git check-ignore subprocess and no Git repository required, which is why an unversioned folder behaves the same way a checkout does.
.copytreeignore uses .gitignore syntax and adds exclusions on top of everything already in force. .copytreeinclude has the highest precedence of any file-based rule and overrides all of them, including .gitignore and profile excludes — the usual reason to reach for it is a hidden directory like .github/ that you want in the bundle.
copytree ignore context . # inventory the tree and the rules already in force
copytree ignore init . # print a conservative starter file
copytree ignore check . # validate the rules and show what they remove Formats, Budgets and Secrets
Output formats
--format takes xml (the default), markdown, json, ndjson, sarif, or tree. XML suits AI consumption, Markdown reads well to a human, and tree gives structure alone — a cheap way to orient an agent before it reads into specific files. Each format is versioned, so the bundle names its own schema (copytree-xml@1) and a downstream prompt can detect a change.
--no-content is the other way to get structure without bodies: it keeps the metadata every format carries but drops the file text.
Budgets
Budgets bind. A file that survives every filter is still dropped when a budget bites, and the run says which one bit and how many files went with it.
| Option | Default | Bound |
|---|---|---|
--size-gate <size> | 256KB | Per file, decided from stat() before the file is opened. |
--max-total-size <size> | — | Total selected bytes. |
--max-files <count> | — | Selected file count, applied after sorting. |
--max-chars <count> | — | Emitted content characters, cut at line boundaries. |
Because a budget keeps the head of the sorted list, --sort and --order decide what survives it. Asking for newest-first is how you keep the files you were actually working on:
copytree --max-total-size 2MB --sort modified --order desc The size gate is a policy, not a safety ceiling: --no-size-gate lifts it, and CopyTree's own hard per-file limit stays where it is.
Secrets
CopyTree scans for credentials on the way out. The default policy is redact, which replaces what it finds and continues. --secrets fail stops the run instead, which is the setting a CI job wants; --secrets off disables the scan. --redaction chooses the marker style (typed, generic, or hash), and --secrets-report <file> writes the structured findings out for a build to archive.
Three more failure policies exist for automated runs, and --strict turns on every applicable one: --fail-empty when nothing was selected, --fail-on-truncation when a budget dropped requested content, and --fail-on-fs-errors when filesystem work degraded after retries. An empty selection is not an error otherwise — a fully-ignored folder returns a valid empty bundle.
Profiles and Configuration
A profile is a repository's own statement about what may leave it, so a checked-in .copytree.yml is discovered automatically and used without being asked for. --no-profile skips that discovery.
name: api
include: ['src/**/*.ts', 'README.md']
exclude: ['**/*.test.ts']
output:
format: markdown Named profiles are read from .copytree-api.yml beside the project root, or .copytree/api.yml in a directory of them. A named profile overlays the automatic one rather than replacing it, so passing --profile api does not quietly discard the shared rules in .copytree.yml:
copytree --profile api -o api-context.md User preferences are data rather than code, in the conventional location for the platform:
| Platform | Configuration file |
|---|---|
| macOS | ~/Library/Application Support/CopyTree/config.yaml |
| Linux | $XDG_CONFIG_HOME/copytree/config.yaml, or ~/.config/copytree/config.yaml |
| Windows | %APPDATA%\CopyTree\config.yaml |
copytree config show --sources # which file each effective value came from
copytree config validate
copytree config migrate --write # convert a legacy ~/.copytree/*.js setup The legacy ~/.copytree/*.js directory is still read, with a warning. Executing JavaScript from a home directory inside a host process is inappropriate for an embedder and unreproducible for everyone, which is why the data file replaced it; config migrate converts an existing setup and leaves the old directory untouched.
Recipes
# Only what you have changed, for a review conversation
copytree --modified
copytree --changed main
# One folder, but with the whole repository's ignore rules
copytree --scope src/panels/file-browser
# Several entries at once; paths are literal, so no glob escaping
copytree --scope "src/[draft]" package.json
# Narrow by language, drop the noise
copytree --include "**/*.ts" --include "**/*.svelte" --exclude "node_modules"
# Into a pipe, or into a file
copytree --stdout --format markdown > context.md
copytree --stdout --format json | jq '.metadata'
# Structure first, contents later
copytree --format tree --stdout
copytree --no-content
# A CI job that refuses to send a truncated or leaky bundle
copytree --stdout --strict --secrets fail --max-total-size 2MB Alongside Daintree
Everything the CLI does, Daintree does too — through the toolbar, a worktree card, a file row, or an agent over MCP. What the app adds is per-project settings, a re-runnable copy history, and terminal injection. What the CLI adds is a shell.
The vocabulary lines up, which matters when you configure one and use the other:
| CLI | In Daintree |
|---|---|
--include | filter / includePaths, and the always-include globs in project settings |
-x, --exclude | exclude, and the excluded paths in project settings |
--force-include | always |
--scope | scopePaths, and Copy context on a file or folder row |
--modified / --changed | modified / changed, and Modified Files Only on a worktree card |
--max-total-size, --max-files, --max-chars | The size and character budgets in Project Settings → Context |
copytree plan | Test config |
Two differences are worth knowing. Daintree resolves settings per project and per window, so the same options mean different things coming from different views — the CLI has only the directory you ran it in. And the app is pinned to the 0.17 line of the library while the CLI on this page is 1.0, so a flag here may not have an app equivalent yet. CopyTree in Daintree documents the in-app surface in full.
Embedding it
CopyTree is built to be embedded, not just run, and that is how Daintree uses it. The SDK and the CLI share one selection path, so what copy() returns is what copytree would have printed:
import { copy } from 'copytree';
const result = await copy(repoRoot, {
scope: ['src/panels/file-browser'],
maxTotalSize: 2_000_000,
});
console.log(result.output);
console.log(result.stats.estimatedTokens); SDK calls are hermetic by default — packaged defaults only, so the same inputs produce the same context on every machine, rather than picking up whatever is in the host user's configuration. Errors are typed and switch on error.code, a streaming form exists for large exports, and full types ship with the package. The repository has the reference, and is where to file an issue.