skrub.
skrub

clean code & product sense for coding agents.

skrub deletes the dead code your agent leaves behind, points it back at what the repo already has, and starts every session knowing what the product is and whose patterns to follow.

44 of 99 lines

on one real pull request: 8 files, 99 added lines. skrub found 44 of them the agent didn't need: 13 safe to delete and 4 judgment calls for you, plus 3 places it rewrote code the repo already had. review them yourself.

01🤔 what i think about code

  1. ievery line is context.

    clean code can sound like a standard from old style books, but agents build on whatever is already there. if the base is strong, so is everything built on it: better code is better context for every agent that touches it next. (on clean code, the same agent used 7% fewer tokens and re-opened files 34% less.)

  2. iireduce, reuse, recycle.

    it belongs in code too: reuse what's already there, add an abstraction only when it earns its place, and name things so they can be found again.

  3. iiiless code isn't always better code.

    deleting and reusing can go too far. ten screens sharing one api call looks tidy, until that call does heavy backend work and each screen throws most of it away. so skrub only deletes on its own what knip finds unused, then runs the typecheck and puts back anything that broke. anything that needs judgment, like a duplicate or a wrapper that looks pointless, it hands to you.

  4. ivthe repo's habits beat anyone's taste.

    models keep changing and getting better. i can't write better code than a frontier model, but i can give it a baseline: the conventions this codebase already follows, read from the code itself.

  5. vif a tool can check it, it doesn't belong in a prompt.

    a rule in a CLAUDE.md is a request, not a guarantee: across 481 of those files, only 4.4% of the security rules had anything actually enforcing them. so skrub turns what it can into checks, and leaves the writing for judgment calls.

  6. vibuild like a product engineer.

    the best code is the change the user actually needed. before it builds, the agent works out the problem behind the ask, and copies what's conventional in its space: for a devtool, how sentry, braintrust or posthog do it. users already know those patterns, so they don't have to learn yours.

what the agent wrote99 lines
src/cli.ts
import { type Report, loadRows, sortBy } from "./report.ts";
⋯
import { toCsv } from "./export/index.ts";
import csvName from "./export/csvName.ts";
⋯
// Write the CSV file next to the report.narrates
if (flag === "--csv") {
await writeFile(csvName(report), toCsv(report));
}
// Export the latest copy from the reports service.
if (flag === "--csv-latest") {
const res = await fetch(`https://reports.example.com/api/reports/${sortField}`);endpoint fetched elsewhere
const latest = (await res.json()) as Report;
await writeFile(csvName(latest), toCsv(latest));
}
src/export/csv.ts
import { type Report, type Row, loadRows } from "../report.ts";
import { formatDate } from "../lib/format.ts";unused import
// Convert the report rows into CSV text.narrates
export function toCsv(report: Report): string {
const head = report.columns.map(escapeCell);
const body = getRows(report).map(formatRow);
const totals = sumColumns(report);
const foot = report.columns.map((column) => String(totals[column] ?? ""));
return [head.join(","), ...body, foot.join(",")].join("\n");
}
function sumColumns(report: Report): Record<string, number> {like columnTotals
const sums: Record<string, number> = {};
for (const column of report.columns) {
sums[column] = report.rows.reduce((total, row) => {
const value = row.values[column];
return typeof value === "number" ? total + Math.round(value) : total;
}, 0);
}
return sums;
}
function getRows(report: Report): Row[] {pass-through
return loadRows(report);
}
// Escape a cell.narrates
function escapeCell(value: string): string {
if (!/[",\n]/.test(value)) return value;
return `"${value.replace(/"/g, '""')}"`;
}
export function formatRow(row: Row): string {export not needed
return row.cells.map((cell) => escapeCell(String(cell ?? ""))).join(",");
}
export function sortRows(rows: Row[], key: string): Row[] {copy of sortBy
return [...rows].sort((a, b) => {
const x = String(a.values[key] ?? "");
const y = String(b.values[key] ?? "");
return x.localeCompare(y);
});
}
export function formatRowLegacy(row: Row): string {never used
const out: string[] = [];
for (const cell of row.cells) {
out.push(cell === null ? "" : String(cell));
}
return out.join(";");
}
export const CSV_VERSION = 1;never used
src/export/csvName.ts
import type { Report } from "../report.ts";off-convention
import { slugify } from "../lib/text.ts";
export default function csvName(report: Report): string {off-convention
return `${slugify(report.title)}.csv`;
}
src/export/draft-export.ts
// First pass at exporting. Replaced by csv.ts.never imported
import type { Report } from "../report.ts";
export function exportReport(report: Report, format: "csv" | "json"): string {
return format === "csv"
? report.rows.map((row) => row.cells.join(",")).join("\n")
: JSON.stringify(report.rows);
}
src/export/index.ts
export * from "./csv.ts";only re-exports
src/export/json.ts
import type { Report } from "../report.ts";never imported
export function toJson(report: Report): string {
const rows = report.rows.map((row) => row.cells);
return JSON.stringify({ title: report.title, columns: report.columns, rows }, null, 2);
}
src/lib/format.ts
// helperrestates the name
export function helper(value: number): string {never used
return value.toFixed(2);
}
src/report.ts
import { sortRows } from "./export/csv.ts";unused import
// TODO: implement csv exportstale todo
export const data = { exportEnabled: true };never used
real scan · nothing is changed[ try the review → ]

02in the agent's loopnpx @amishaba/skrub init

  1. startit knows the product.

    every session starts with what the product is, who uses it, and whose patterns to follow. on its first session in agentdiff, the agent wrote:

    • who uses itai engineers checking whether a prompt, tool or model change altered what their agent does, before it merges
    • model onbraintrust, langsmith, promptfoo
  2. writeit reuses what's there.

    before the agent adds csv.ts, skrub points it at what the repo already has:

    • sumColumns85% the same as columnTotals
    • sortRows100% the same as sortBy

    in this demo the agent writes the file anyway, so the rest of the story has something to clean up.

  3. done?skrub checks the change.

    the agent wrote 99 lines across 8 files. skrub finds 44 it didn't need: an import nothing uses, a file nothing imports, a comment that only repeats the code, and more. it says so once, in at most ten lines.

  4. fixthe safe ones go.

    skrub fix deletes the 13 nothing uses, runs the typecheck, and puts back anything that broke.

  5. youthe rest are your call.

    4 judgment calls, like a wrapper or a copy of a helper, wait for you: delete them, or keep one with a reason. skrub never asks twice, and a clean change never stops at all.

init sets up the product and conventions in .claude/conventions.md, three hooks (session start, before a new file, before the agent stops), and a skill with a product engineer's guide.

03does it help?8 real repos and live claude code runs · oct 2026

  1. fix8 of 8 real repos stayed green.

    every safe finding applied at once, then typecheck, tests and build still pass. the version before knip broke 4 of them.

  2. fix33 edits on agentdiff, nothing broke.

    dead code and exports nothing else needed, from one fix. typecheck, tests, build and lint all still pass.

  3. claude code1 new file instead of 2.

    the same csv task, with and without skrub. with it, the agent read the product guide, ran fix, and said no to a flag that would have done nothing.

  4. claude codeno copies.

    told to put its summing logic in a new file, the agent reused columnTotals instead, and said why.

  5. first run3 products to model on.

    on its first session in agentdiff, the agent picked braintrust, langsmith and promptfoo. you approve the edit, so you can correct it.