sarah
Sarah is our beloved mother that tracks our behavior and make sure we don't screw up. Enforcing tokens , line count, folder structure and more.
Counts tokens, lines and function sizes per file so AI agents (and humans) can see when, what and how to refactor a repo.
Built on existing crates: ignore (gitignore-aware parallel walking), bpe-openai
(tokens; exact tiktoken counts), tokei (code/comment/blank lines), tree-sitter-tags
(function spans), rayon (parallelism; files over 256 KB are tokenized in chunks).
Install
Download a binary for Linux (x86_64/aarch64, static musl), macOS (Apple Silicon/Intel) or Windows (x86_64) from Releases, or:
Usage
Key flags: --sort tokens|lines|name, -n/--top N, --tokenizer o200k|cl100k,
--max-file-tokens 8000, --max-file-lines 500, --max-fn-lines 80,
--max-fn-tokens N and --max-fn-depth N (off by default),
-i/--include GLOB, -e/--exclude GLOB, --hidden, --no-functions,
--no-default-excludes (lockfiles, *.min.js, *.map are skipped by default),
--config FILE (default: <PATH>/sarah.toml when it exists), --no-allow (ignore sarah-allow comments).
Exit codes: 0 success, 1 warnings with --fail-on-warn, 2 error.
Function extraction: Rust, Python, JavaScript, TypeScript/TSX, Go, Java, C, C++, C#,
Ruby, PHP. Each function has a nesting depth: the deepest chain of if/loop/match/
switch/try blocks (else if chains count once).
The JSON report also has directories: per folder, its direct files and dirs, and
total_files, lines and tokens for the whole subtree.
Allow comments: sarah-allow
Turn off function or file warnings in the code, like clippy's #[allow]:
// sarah-allow(large_function, deep_function): one flat state machine
/// Docs, attributes and decorators may sit between the comment and the function.
# sarah-allow-file(large_file): generated
sarah-allow(kinds): reasongoes directly above a function:large_function,deep_function.sarah-allow-file(kinds): reasongoes anywhere in a file:large_file,large_function,deep_function.- No list (
sarah-allow: reason) allows every kind of that scope. - The
:is required; the reason after it may be empty. - Any line-comment style works (
//,#,--,/* */); the comment must be on its own line. - A malformed directive, or one above no function, is a
bad_allowwarning. - Allowed warnings are removed before
--baselineand--fail-on-warn. The report shows "N warnings allowed by sarah-allow comments" (JSON:allowed);--no-allowshows them all. - Folder rules have no comment; override them per glob in
sarah.toml.
Config: sarah.toml
Optional. Without it, sarah behaves as above. CLI flags win over the file.
= "rust" # rust | python | go | ts: built-in folder rules, applied first
= ["vendor/**"] # added to --exclude (include = [...] too)
# Size limits per glob. Later matches override earlier ones, field by field.
[[]]
= "**"
= 60
= 4
[[]]
= "tests/**"
= 200 # max_file_tokens, max_file_lines, max_fn_tokens also work
# Folder rules. Later matches override earlier ones, field by field.
[[]]
= "src/**" # `*` = one path part, `**` = any; the root is "." (or "**")
= 12 # files directly in the folder
= 8 # sub-folders directly in the folder
= 4 # levels below the root; reported once per branch
= 40000 # tokens in the whole subtree (one agent context)
= 2 # direct entries (files + folders); fewer = flatten it
= ["mod.rs"] # each glob must match a file in the folder
= ["utils*", "misc*"] # no file or folder name may match
= ["cli", "walk"] # sub-folder names must match one of these
= "snake_case" # snake_case | kebab-case | camelCase | PascalCase
= true # no `x.rs` next to `x/`
[[]]
= "src/generated/**"
= 100
= [] # an empty list turns a rule off
Folder warnings use kinds such as folder_too_many_files and folder_bad_name. They
work with --fail-on-warn, --baseline, --changed-since and annotations.
--baseline matches warnings by path, kind and subject (function name or glob), and
shows a warning again when its file, function or folder grew. --changed-since REV
compares with the merge base of REV and HEAD, and counts uncommitted and untracked
files.
Token counts use OpenAI encodings; Claude's tokenizer isn't public, so treat them as a close approximation.
Symlinks: every file is counted once, keyed by its realpath. File links are followed
when their target is inside the root; directory links are never descended (an in-root
target is walked by its real path anyway), so link cycles can't loop. Broken links,
links leaving the root, directory links and duplicate targets are reported under
skipped instead of disappearing.
GitHub Action
Installs the released binary for the runner (Linux, macOS, Windows; x64 and ARM64 where released), writes the markdown report to the job summary and exposes the JSON report.
- uses: actions/checkout@v7
- id: sarah
uses: aiman2039/sarah@v0.1.3 # a release tag; @master installs the latest release
with:
path: .
args: --max-fn-lines 60 --exclude 'vendor/**'
fail-on-warn: true
- run: echo "${{ steps.sarah.outputs.tokens }} tokens, ${{ steps.sarah.outputs.warnings }} warnings"
| Input | Default | |
|---|---|---|
path |
. |
Directory to analyze |
args |
Extra sarah arguments |
|
fail-on-warn |
false |
Fail the step when limits are exceeded |
summary |
true |
Write the markdown report to the job summary |
annotations |
true |
Annotate the files (and lines) that have warnings |
version |
action ref if it is a tag, else latest | Release tag to install |
token |
github.token |
Token used to download the release |
Outputs: json (report path), tokens, warnings.
Development
Hooks install on the first cargo test (cargo-husky). pre-commit runs
scripts/check.sh fmt and scripts/check.sh clippy. Clippy runs pedantic plus the
safety lints in [lints.clippy] in Cargo.toml, with -D warnings.
Releasing
&&
The Release workflow (.github/workflows/release.yaml) runs CI on macOS/Linux/Windows,
builds all five targets in parallel, uploads them to a draft release with
SHA256SUMS.txt, publishes it, then runs the action end to end against the new release.
CI runs only as part of Release.