prune-os
v0.8.0
Published
Code simplification tool - same functionality, less complexity
Downloads
1,056
Maintainers
Readme
PruneOS
Same code, less of it.
PruneOS is a fast, zero-config code simplifier for TypeScript and JavaScript.
It removes verbose patterns that AI-generated code (and humans) tend to write,
without changing behaviour: redundant === true, x !== null && x !== undefined,
nested if/else chains that should be guard clauses, unused imports, duplicate
same-source imports, and more.
It has two modes:
pruneos <files>: per-file simplifier. Removes line-level verbosity. Fast.pruneos audit: repo-wide deletion-first audit. Finds unused exports, single-use abstractions, overengineering patterns (*Manager,*Factory, empty interfaces, one-method classes), AI slop (// TODO, mock data, empty rethrows), and over-fragmented component directories.
It also ships a Claude Code skill so AI agents working inside your project run both modes automatically. See For AI agents below.
Example
A real function before and after pruneos -a --fix, 49 lines down to 42, six
distinct transformations across the diff. Behaviour is identical.
// before (49 lines)
import { fetchUser } from './api';
import { logEvent } from './log';
import { logError } from './log';
import { Cache } from './cache';
const cache = new Cache<string, string>();
export async function getDisplayName(id: string, fallback: string): Promise<string> {
if (id === null || id === undefined || id === '') {
return fallback;
}
const cached = cache.get(id);
if (cached !== null && cached !== undefined) {
return cached;
}
const user = await fetchUser(id);
if (user === null || user === undefined) {
logEvent({ type: 'user.miss', id: id, fallback: fallback });
return fallback;
}
let display: string;
if (user.name !== null && user.name !== undefined && user.name !== '') {
display = user.name;
} else {
if (user.email !== null && user.email !== undefined) {
display = user.email;
} else {
display = fallback;
}
}
cache.set(id, display);
return display;
}// after (42 lines)
import { fetchUser } from './api';
import { logEvent } from './log';
import { Cache } from './cache';
const cache = new Cache<string, string>();
export async function getDisplayName(id: string, fallback: string): Promise<string> {
if (id == null || id === '') return fallback;
const cached = cache.get(id);
if (cached != null) return cached;
const user = await fetchUser(id);
if (user == null) {
logEvent({ type: 'user.miss', id, fallback });
return fallback;
}
let display: string;
if (user.name != null && user.name !== '') {
display = user.name;
} else if (user.email != null) {
display = user.email;
} else {
display = fallback;
}
cache.set(id, display);
return display;
}What changed: unused logError import dropped, triple-empty checks collapsed,
dual !== null && !== undefined collapsed to != null, single-statement if
blocks made one-line, } else { if (...) } flattened to else if, object
property shorthand applied. Six rules; one diff; nothing else touched.
Install
# global
npm install -g prune-os
# or one-shot
npx prune-os src/ -r --fixRequires Node 18+.
For AI agents
If you let an AI agent write code in your project, install the Claude Code
skill that ships inside prune-os:
npx prune-os install-skill # writes .claude/skills/pruneos/SKILL.md
npx prune-os install-skill --global # writes ~/.claude/skills/pruneos/SKILL.mdThe skill tells the agent: after writing or editing any TS/JS file in this
project, run npx prune-os <those files> --check, and apply --fix if it
reports changes.
The effect: the agent's first draft still looks like an LLM wrote it
(defensive null checks, === true, redundant ternaries, single-statement
if blocks), but the agent strips that boilerplate before you ever read
the diff. You get the cleaner version on the first pass.
The skill is auto-discovered by Claude Code from .claude/skills/. It works
the same as any other skill: the agent reads its description, decides when
to apply it, and runs the documented commands.
You don't need the skill to use prune-os. The CLI works standalone.
60-second tour
pruneos app.ts # simplify one file in place
pruneos src/ -r --fix # simplify everything under src/
pruneos app.ts --diff # preview the diff, don't write
pruneos src/ -r --check # CI mode, exit 1 if anything would change
pruneos src/ -r --json # machine-readable report
pruneos audit # cross-file audit (unused exports, slop, ...)
pruneos audit --fix --json # apply safe auto-fixes, emit JSON
pruneos init # write pruneos.config.ts
pruneos install-skill # install the Claude Code skillAudit (cross-file deletion-first scan)
pruneos audit walks the repo, builds an export/import graph, and reports five
categories of findings:
| Category | What it catches | Auto-fix |
| -------------------- | ---------------------------------------------------------------------------- | ----------------- |
| unused-export | Exports nobody imports anywhere | Yes (--fix) |
| single-use-export | Exports used by exactly one file. Inline candidate. | No (advisory) |
| overengineering | *Factory, *Manager, *Provider, *Adapter with 0-1 uses. Empty interfaces. One-method classes with no state. | No (review) |
| slop | // TODO, // FIXME, // AI generated markers. mockX/dummyX/fakeX in non-test files. Empty rethrows. Empty catch {}. | Rethrows yes |
| fragmentation | Foo/{Foo.tsx, Foo.types.ts, Foo.utils.ts, index.ts} patterns. Suggests collapsing into one file. | No |
Example output on a deliberately-bad fixture:
PruneOS audit v0.8.0
────────────────────────────────────────────────
Unused exports (dead code) (8)
lib/format.ts:12 `unusedHelper` is exported but never imported anywhere
lib/UserManager.ts:2 `UserManager` is exported but never imported anywhere
...
Single-use exports (consider inlining) (2)
lib/format.ts:7 `capitalize` is exported but used by exactly one file
Overengineering patterns (4)
lib/format.ts:12 `unusedHelper` uses the `Helper` pattern but has no callers
lib/UserManager.ts:2 `UserManager` uses the `Manager` pattern but has no callers
lib/UserManager.ts:9 `interface EmptyConfig` is empty
lib/UserManager.ts:2 `class UserManager` has exactly one method and no state
AI slop / leftover markers (6)
lib/api.ts:3 try/catch that rethrows the same error unchanged
lib/slop.ts:1 Leftover marker comment: // TODO: replace with real logic
...
File fragmentation (1)
components/Button/Button.tsx:1 `Button/` is split across 4 files. Consider one file.
Total: 21 findings across 10 files.The audit respects package.json main/module/types/exports/bin and
any src/index.ts at the root. Exports re-exported through those entry points
are considered public API and never flagged. Add --only unused-export,slop to
filter categories, --json for machine-readable output, --fix to apply the
safe transforms (delete unused exports, remove empty rethrows).
Limitations: regex parsing, not a TS AST. Doesn't follow dynamic import('./x')
or tsconfig paths aliases. Symbols used only via those patterns may show up
as unused. Fix by adding a static import or moving them into the public entrypoint.
What it changes
| Pattern | Becomes |
| -------------------------------------------------- | -------------------------------- |
| x === true / x !== false | x |
| Boolean(x) / !!x | x |
| x !== null && x !== undefined | x != null |
| x === null \|\| x === undefined \|\| x === '' | x == null \|\| x === '' |
| x !== null ? x : y | x ?? y |
| if (cond) { return X; } else { return Y; } | if (cond) return X; return Y; |
| if (cond) { return X; } | if (cond) return X; |
| (args) => { return EXPR; } | (args) => EXPR |
| { foo: foo, bar: bar } | { foo, bar } |
| } else { if (...) { ... } } | } else if (...) { ... } |
| void 0 | undefined |
| Multiple import { a } from 'x' from same module | One merged import |
| Unused imports / unused locals | Removed |
| Runs of blank lines, trailing whitespace | Collapsed |
Everything that's removed or rewritten is reported as a change in the JSON
output, so it's auditable.
What it preserves
- Functions, exports, types, interfaces, JSDoc.
- Public API of every module.
- Comments (unless they are commented-out code on their own line).
- The runtime behaviour of the original file.
If a transform isn't provably safe with a simple text rewrite, PruneOS doesn't do it. The goal is "I'd be happy to merge this diff," not "trust me."
Benchmark
npm run bench runs PruneOS over fixtures in bench/fixtures/. The
fixtures are real AI-generated code: full React components with hooks
and JSX, plus standalone utility modules.
Median of 5 runs, Node 24, Windows x64. Counts only real removals. Blank lines that were already in the source are preserved.
A full mini SaaS website
bench/fixtures/website/ is a small Next.js-style
landing site: 14 files, 898 lines. Layout, two pages (home + pricing), nine
components (header, footer, hero, feature grid, pricing card, testimonial,
newsletter form, FAQ list, cookie banner) and a lib/ folder.
pruneos bench/fixtures/website -r -a --fix
→ 14 files, 66 lines saved, 61 changes, 12ms (7.3% reduction)Per-file range is 0-36%. Heaviest wins on utility files (lib/format.ts
at 36%, lib/api.ts at 15%) and the pricing card / hero components.
React components (smaller scope)
| Fixture | Lines | After | Saved | % | Changes | Time | | ---------------------------- | ----- | ----- | ----- | ----- | ------- | ---- | | components/UserDashboard.tsx | 167 | 148 | 19 | 11.4% | 15 | 1ms | | components/TodoList.tsx | 202 | 176 | 26 | 12.9% | 7 | 1ms | | components/SearchBar.tsx | 190 | 176 | 14 | 7.4% | 5 | 1ms | | React subtotal | 559 | 500 | 59 | 10.6% | 27 | 3ms |
Functional components with useState/useEffect/useCallback/useMemo,
conditional JSX, event handlers, and the verbose patterns AI likes to
emit (x !== null && x !== undefined, if (cond) { return X; } else
{ return Y; }, { foo: foo }). PruneOS removes the patterns it can
prove are safe and leaves the rest. Numbers count line reductions only;
they do not include in-line shortenings (e.g. a 5-line if becoming
a 1-line guard) which dominate the actual diff.
Other patterns
| Fixture | Lines | After | Saved | % | Changes | Time | | ---------------------- | ------ | ------ | ----- | ----- | ------- | ---- | | boolean-noise.ts | 307 | 215 | 92 | 30.0% | 61 | 3ms | | react-patterns.ts | 1,673 | 1,664 | 9 | 0.5% | 19 | 8ms | | notification-system.ts | 2,094 | 2,092 | 2 | 0.1% | 4 | 14ms | | state-store.ts | 2,695 | 2,692 | 3 | 0.1% | 15 | 13ms | | data-pipeline.ts | 3,765 | 3,752 | 13 | 0.3% | 16 | 23ms | | Total (all) | 11,991 | 11,747 | 244 | 2.0% | 201 | 70ms |
boolean-noise.ts is a synthetic upper bound on what's possible when the
input is dense with patterns PruneOS knows. The four utility files
(react-patterns, notification-system, state-store, data-pipeline)
are 70%+ JSDoc and type definitions; PruneOS preserves both, so the
percentage there is small by design.
If you're running PruneOS on actual component code, expect line reductions in the 7-15% range with zero behavioural risk. The diff itself is larger than the line count suggests, because most transforms shorten lines rather than removing them. Throughput is around 170k lines/second on commodity hardware.
The bench/RESULTS.md file is regenerated on every npm run bench and
contains the full table plus per-issue detection counts.
Reproduce
git clone https://github.com/Hi9841/PruneOS.git
cd PruneOS
npm install
npm run benchSet PRUNE_BENCH_ITERATIONS=20 npm run bench for a more stable median.
Comparative benchmark: deletion-first vs default AI
The CLI benchmark above measures one thing: how much PruneOS shrinks a file
after the code is written. The comparative benchmark in
benchmarks/ measures something different and more
interesting: how big is the diff an AI produces in the first place when
deletion-first constraints are active.
For each scenario the same task is solved twice from the same starting
files. Baseline is a default AI session. PruneOS is the same model with the
deletion-first system prompt and the PruneOS Claude Code skill
active. The runner then diffs each output against the starting fixture with
git diff --no-index --numstat.
Results
Three scenarios. Single-model self-play (Claude Opus 4.7). Behaviour verified by reading the diff, not by running tests. See caveats below before quoting these numbers.
| Scenario | Baseline files / new / net LOC | PruneOS files / new / net LOC | | ------------------------ | -----------------------------: | ----------------------------: | | complex-conditional | 2 / 1 / +26 | 1 / 0 / -19 | | duplicate-helper-cleanup | 4 / 2 / +7 | 3 / 1 / -5 | | minimal-bug-fix | 1 / 0 / +17 | 1 / 0 / +0 | | Suite total | 7 / 3 / +50 | 5 / 1 / -24 |
In every scenario the PruneOS run touched fewer or equal files, introduced
fewer new files, and produced a smaller net diff. On minimal-bug-fix the
baseline fixed the bug and refactored the surrounding function; the
PruneOS run changed the one line that contained the bug.
What the runs actually did
- complex-conditional. Baseline extracted a
PermissionRuleinterface and aPERMISSION_RULESarray in a new file. PruneOS rewrote the function as guard clauses in place. Same boolean output for the same inputs. - duplicate-helper-cleanup. Baseline created a
utils/directory with a barrel re-export and JSDoc. PruneOS added onedate.tsexporting one function. - minimal-bug-fix. Baseline rewrote the loop as
reduce, extracted alineTotalhelper, and added JSDoc. PruneOS changeditem.pricetoitem.price * item.quantity.
Caveats
- Self-play bias. Both sides are the same model. A different model, or a real engineer driving the baseline, may close the gap or invert it.
- Small fixtures. 10–25 LOC of starting code per scenario, chosen to be situations where AI overengineers. Real engineering work is bigger and messier.
- No automated correctness check. Behaviour preservation was verified by reading the diff. Not the same as running a test suite.
- LOC is not quality. Smaller diffs are reported as smaller diffs. A PruneOS run that won on lines by breaking behaviour would be a loss.
The full per-run breakdown, methodology, and a script to reproduce the
numbers live in benchmarks/RESULTS.md and
benchmarks/README.md. To re-run:
node benchmarks/measure.mjsCLI reference
| Flag | Description |
| --------------------- | ----------------------------------------------------- |
| -r, --recursive | Recurse into directories |
| -a, --aggressive | Run multiple simplification passes |
| -f, --fix | Write changes to disk (on by default unless --dry-run) |
| -d, --dry-run | Preview without writing |
| --diff | Show a unified diff per file (implies --dry-run) |
| -c, --check | CI mode - exit 1 if any change would be made |
| --json | Emit a machine-readable JSON report on stdout |
| -v, --verbose | Per-file output |
| -s, --silent | Suppress all non-error output |
| -e, --extensions | Comma-separated extensions, defaults to .ts,.js,... |
| --ignore | Comma-separated dirs to skip |
| -h, --help | Show help |
| -V, --version | Show version |
Programmatic API
import { analyze, simplify } from 'prune-os';
const code = await fs.promises.readFile('app.ts', 'utf-8');
const a = analyze(code);
console.log(a.metrics.cyclomaticComplexity, a.issues.length);
const r = simplify(code, { aggressive: true });
console.log(`saved ${-r.metrics.netChange} lines, ${r.changes.length} changes`);
await fs.promises.writeFile('app.ts', r.simplified);simplify returns { original, simplified, changes, metrics, issues }.
analyze returns the same metrics PruneOS uses internally (LOC, cyclomatic
and cognitive complexity, Halstead volume, maintainability index, unused
imports/vars, deep nesting, magic numbers).
CI integration
GitHub Actions:
- run: npx prune-os src/ -r --checkPre-commit hook:
npx prune-os "$(git diff --cached --name-only --diff-filter=AM | grep -E '\.(ts|tsx|js|jsx)$' | xargs)" --fixConfiguration
pruneos init writes a starter pruneos.config.ts. Override the defaults
you want, leave the rest:
export default {
rules: {
'no-unused-imports': { enabled: true, severity: 'warning' },
'no-magic-numbers': { enabled: false },
},
thresholds: {
maxLineLength: 120,
maxFunctionLength: 40,
maxComplexity: 15,
},
ignore: ['node_modules', 'dist', '.next'],
};Status
PruneOS is at v0.5. It's safe to run on real codebases (every transform is text-level and reversible via git), but the rule set is still growing. If you hit a pattern that should be simplified but isn't, open an issue with a minimal repro.
License
MIT - see LICENSE.
