astcount measures source code through its Tree-sitter syntax tree. A syntax
tree keeps the structure of a program while ignoring whitespace, line wrapping,
and identifier length. Counting its nodes gives a rough estimate of how much
code is actually there, rather than how much space the text takes up.
Install all three skills globally for Codex with the skills.sh CLI:
bunx skills add wokalski/astcount --skill '*' --agent codex --global --yesThen open Codex in the repository you want to change and include the skill and task in the same prompt. These are Codex prompts, not shell commands:
$astcount-refactor-interactive Inspect this repository, propose refactors, and ask me about correctness and performance tradeoffs.
$astcount-refactor-loop Refactor this repository autonomously until credible structural gains are exhausted.
$astcount-verified-refactor-loop Refactor src deeply. Use `bun test` as the exact deterministic test command and keep digging until no credible structural improvement remains.
astcount-refactor-interactiveasks before correctness, API, or performance tradeoffs.astcount-refactor-loopkeeps simplifying autonomously.astcount-verified-refactor-loopadditionally gates every candidate with one exact test command.
All three freeze the astcount policy before measuring and understand selectors, test exclusions, ast-grep patterns, and Tree-sitter queries.
skills use can instead download one skill and open a temporary Codex session:
bunx skills use wokalski/astcount@astcount-refactor-interactive --agent codex
bunx skills use wokalski/astcount@astcount-refactor-loop --agent codex
bunx skills use wokalski/astcount@astcount-verified-refactor-loop --agent codexThe temporary session loads the skill, then waits for your refactoring request.
Run the native binary with Bun:
bunx astcount .The package downloads the matching native Rust binary without a lifecycle script. It supports Linux glibc and macOS on x64 and arm64.
Run directly or install permanently with Nix, using the public Cachix cache:
nix run --extra-substituters https://astcount.cachix.org --extra-trusted-public-keys astcount.cachix.org-1:NgwAPl0WX9xB3qatDahUC8T0R9jcEuwOFhgdrwV/lk8= github:wokalski/astcount -- .
nix profile install --extra-substituters https://astcount.cachix.org --extra-trusted-public-keys astcount.cachix.org-1:NgwAPl0WX9xB3qatDahUC8T0R9jcEuwOFhgdrwV/lk8= github:wokalski/astcountCount the current directory, or name a narrower source tree:
astcount .
astcount count srcAdd a per-file breakdown when you want to find the largest files. Rows are
sorted by selected node count, with the largest files last. --stats adds the
operational summary below the table:
astcount count . --files
astcount count . --files --statsUse --stream when seeing results immediately matters more than sorting. Human
rows are printed as parsing completes; combined with --json, each line is a
JSON file event followed by one summary event:
astcount count . --stream
astcount count . --stream --jsonParsing is parallel by default. Override the worker count when needed:
astcount count . --threads 4The default metric includes every Tree-sitter node. Exclude node kinds or properties to focus the measurement:
astcount count . --exclude-kind anonymous
astcount count . --exclude-kind anonymous,extra
astcount count . --exclude-kind extra,error,missingDiscover the grammar-specific types in the final selected population with
--by-type. The histogram is sorted from smallest to largest and includes the
language plus whether each type is named or anonymous:
astcount count src --exclude-kind anonymous --by-typeSelect exact grammar types when measuring particular constructs:
astcount count . --select-type rust=function_item
astcount count . --select-type rust=let_declaration --select-type javascript=variable_declaratorAst-grep selectors are friendlier when a source-shaped construct is easier to describe than its grammar type. Each complete match contributes its root node, not the entire subtree:
astcount count . --select-pattern 'rust=let $NAME = $VALUE;'
astcount count . --select-pattern 'javascript=const $NAME = $VALUE'Tree-sitter selector queries are the precise alternative. Nodes captured as
@select enter the selected population; larger queries can live in files:
astcount count . --select-query 'rust=(identifier) @select'
astcount count . --select-query-file rust=queries/public-functions.scmExclude entire files with repeatable globs. Globs are relative to the current
directory; a pattern without / matches that basename at any depth:
astcount count . --exclude-file 'tests/**'
astcount count . --exclude-file '*.test.*' --exclude-file '*.spec.*'Use the built-in module-test preset to remove conventional in-source tests from Rust, OCaml, JavaScript, and TypeScript counts:
astcount count . --exclude-preset module-testsFor project-specific syntax, ast-grep code patterns are the concise option. Prefix each pattern with its Tree-sitter language name:
astcount count . --exclude-pattern 'rust=mod $NAME { $$$BODY }'
astcount count . --exclude-pattern 'javascript=describe($NAME, $CALLBACK)'Tree-sitter queries provide the precise escape hatch. Every subtree captured as
@exclude is omitted. Put larger queries in a file to avoid shell quoting:
astcount count . --exclude-query 'rust=(function_item) @exclude'
astcount count . --exclude-query-file rust=queries/generated-code.scmUse JSON for automation, or save two complete reports and compare them. The comparison can fail CI when the selected node count grows:
astcount count . --json
astcount count . --save before.json
astcount count . --save after.json
astcount compare before.json after.json --fail-on-increase
astcount compare before.json after.json --jsonBare astcount defaults to astcount count .. For compatibility and quick
interactive use, count options and paths may also omit the count subcommand,
so astcount src --exclude-kind anonymous is equivalent to the explicit form.
Language is detected from filenames, extensions, and shebangs; use
--language rust when detection is ambiguous. Directory walks respect ignore
files such as .gitignore.
- Any selector enables selection mode. Selectors are ORed, overlaps count once, and languages without a matching selector contribute zero.
--select-typeuses exact, grammar-specific names. Unknown types are rejected; use--by-typeto discover them.- Ast-grep selectors count match roots. Tree-sitter selector queries capture
nodes as
@select. - Exclusions apply after selection. Ast-grep matches and Tree-sitter captures
named
@excluderemove complete subtrees. --exclude-kind anonymousmeans named nodes only. Named and anonymous cannot both be excluded; parser diagnostics remain raw.module-testscovers Rust#[cfg(test)] mod, OCaml inline-test forms, and JavaScript/TypeScript/TSXif (import.meta.vitest)blocks. Use file globs for ordinary test files.
Tree-sitter types are language-specific, and whitespace usually is not a node.
“All” means all emitted tree nodes, not all tokens or bytes. Saved reports record
the complete selector/exclusion policy, and compare rejects incompatible
reports. Schema-3 reports remain readable.
nix develop -c cargo test
nix develop -c cargo clippy --all-targets -- -D warnings
nix develop -c node scripts/release.mjs check 0.3.0
nix flake checkRelease automation and public-cache setup are in
RELEASING.md.
Node count is a structural size metric, not a proof of software quality. Compare
the same codebase using the same astcount version, language grammar, flags, and
generated/vendor-file policy. Counts from different languages or grammar
versions are not directly comparable.