feat(tidy): non-destructive repo cleanup, plus 25 defect fixes including a shell injection - #13
Merged
Conversation
Cleans up a cluttered repo and never deletes anything. The engine knows exactly three operations and none of them destroys bytes: quarantine moves a file into .attic/<date>/ with its original path preserved, untrack drops a path from the index while the file stays on disk, and move relocates a file. There is no delete path in tidy-apply.ts. It refuses to run on the default branch or on a dirty tree, so deleting the branch is always a complete escape hatch, and it writes a MANIFEST.json carrying the exact inverse of every operation performed. Dry run is the default; --undo <manifest> --apply replays those inverses, down to removing the .gitignore line the run appended. What makes it more than a filename heuristic is the evidence. tidy-scan.ts builds an import graph over the repo's own sources and walks it from the real entry points (framework routes with or without the src/ layout, config and test files, conftest.py, edge functions, anything with a shebang, anything package.json or a CI workflow executes by path), then combines that with git history and a textual mention sweep. A file is only called an orphan when no entry point reaches it, nothing imports it, and no tracked file even names it. Everything the evidence does not settle is reported as a question rather than an action, and the scanner is deliberately biased toward calling things used: an unresolvable import marks its target reachable, and a prose mention downgrades a quarantine to a review. The flow follows five phases, each ending in a committed artifact under .keepwright/tidy/<date>/, with [NEEDS DECISION] markers that gate the planning phase. Templates for every artifact live in skills/tidy/references/. Verified end to end: four refusal paths that write nothing, a dry run that leaves the tree clean, and an apply plus undo that returns every tracked path and blob hash to its pre-tidy state. Against a clone of a real 555-file project, the project's own TypeScript compiler reports the same zero errors before the run, after quarantining nine files, and after the undo. Autor: Leonardo Candiani
… more defects The mention workflow interpolated the comment body, the issue body and the issue title straight into a run: script inside single quotes. Actions substitutes an expression into the script TEXT before bash parses it, so a comment carrying a quote closed the quoting and the rest of it ran as commands. The step fires on every issue_comment.created, before the mention filter, which lives inside the same already-substituted script and therefore protected nothing. With contents: write and the schema's self-hosted runner default, that was arbitrary command execution on the maintainer's machine, triggerable by anyone who can comment on an issue. Every untrusted field now travels through the step's env: block, the way the issue-triage workflow already did. The same class is closed in the auto-merge branch name, the Supabase deploy input and the auto-review base ref, and CI now fails if any known free-text GitHub context field appears inside a run: block. Three defects broke the product in ordinary use. Every commit failed right after setup: the installed lefthook.yml called a validators runner that no template generated, and carried SOURCE_GLOB and CMD_TYPECHECK as literal text, so the pre-commit type-check tried to execute the token as a command. A repo that already had a CLAUDE.md got a red pipeline on its first push, because the same run installs the rules and the validator that fails when a rule has no pointer; apply now appends the missing pointers without rewriting a line the maintainer wrote. And the review skill globbed rules 01 through 07, so two of the nine were never consulted. The most valuable class was the silent one. With no criticalFiles configured, the auto-review workflow grepped the changed-file list for the literal string CRITICAL_FILE_1, so the warning never fired and the repo looked watched while nothing watched it. The audit counted a repo's own ci.yml as coverage for a pipeline running none of these checks. The banned-terms grep matched an authorship trailer anywhere in a line, and REVIEW.md documents that exact string, so the repo blocked any PR that edited its own review doc. The documented merge bypass expanded to a literal word instead of reading its variable whenever the project name contained a hyphen. And derived patterns, the product's central claim, were mined and declared and then reached nothing, so every PR was still reviewed against a generic ideal. Each lesson became a check with an exit code rather than a comment. apply.ts refuses to install when a placeholder would survive into a file that gets executed, and that gate found the CRITICAL_FILE defect on its first run. The five diverging copies of the secret pattern list became one data file that the shell greps and the TypeScript scanners both read, with the more permissive bound winning wherever two copies disagreed. CI installs the plugin into scratch greenfield and brownfield repos, plants a fake credential and fails if the validator passes it, and fails if an emptied pattern list is accepted. Removed: the orchestration workflows are no longer copied into the target repo, where nothing invoked them and where they could not run; customValidators and mode leave the schema, since a versioned config should describe the repo and not the action being performed on it. Autor: Leonardo Candiani
…d string The consistency check pinned the literal string `grep -E -f scripts/validators/secret-patterns.ere`, which stopped existing when the workflows started stripping comments and blank lines into a temp file before grepping. The workflows were correct; the check had gone stale against a later change in the same branch, and it failed on the first real Actions run. It now asserts the invariant instead of a spelling: each consumer must name the shared file and must grep with -f, and no workflow may carry a credential prefix inline. Verified with a positive control, by removing the read from one consumer and confirming the check rejects it. Autor: Leonardo Candiani
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What this is
Two things: a new command that cleans up cluttered repos without ever deleting
anything, and 25 defects found by reading the plugin end to end, each one
reproduced by running the engine rather than by reading it.
The new command
/keepwright:tidytakes a repo that has filled up with backup files, committedbuild output, byte-identical duplicates, modules nothing imports and an
unreadable root, and leaves it cleaner with every change reversible.
Nothing is ever deleted. The engine knows three operations and none destroys
bytes:
quarantinemoves a file into.attic/<date>/keeping its originalpath,
untrackdrops a path from the index while the file stays on disk, andmoverelocates. There is no delete path intidy-apply.ts, and CI greps thesource to keep it that way. It refuses a dirty tree and the default branch, so
deleting the branch is a complete escape hatch, and
MANIFEST.jsonholds theexact inverse of every operation.
What makes it more than a filename heuristic is the evidence.
tidy-scan.tsbuilds an import graph and walks it from the real entry points, then combines
that with git history and a mention sweep. A file is called an orphan only when
no entry point reaches it, nothing imports it, and no tracked file names it.
Everything else is a question, not an action. The scanner is deliberately
biased toward calling things used: a false "still in use" costs a line of
output, a false "unused" costs someone their code.
Verified: four refusal paths that write nothing, a dry run that leaves the
tree clean, and apply plus undo returning every blob hash to its prior state.
Against a clone of a real 555-file project, the project's own TypeScript
compiler reports the same zero errors before, after quarantining nine files,
and after the undo. An independent grep, written against a different
implementation than the graph, finds zero references to all nine.
The defects
Critical
The mention workflow interpolated the comment body into a
run:script insidesingle quotes. Actions substitutes into the script text before bash parses it,
so a comment with a quote broke out. The step fires on every
issue_comment.created, before the mention filter, which sits inside the samealready-substituted script. With
contents: writeand the schema'sself-hosteddefault, that was arbitrary execution on the maintainer's machineby anyone who can comment on an issue. Fixed here, along with three more of the
same class, and CI now fails on any free-text GitHub context inside a
run:.Broke the product in ordinary use
generated, and carried placeholders as literal text that it tried to execute.
CLAUDE.mdgot a red pipeline on its first push,which is exactly the "works on any existing repo" case the README advertises.
read.
Silent failures, the worst category
criticalFilesset, the workflow grepped for the literal string{{CRITICAL_FILE_1}}: the warning never fired and the repo looked watched.ci.ymlas keepwright coverage.REVIEW.mddocuments that string, so the repo blocked any PR editing its ownreview doc.
variable whenever the project name had a hyphen.
then reached nothing.
Every lesson became a check with an exit code
apply.tsrefuses to install when a placeholder would survive into an executedfile, and that gate found the
CRITICAL_FILEdefect on its first run. The fivediverging secret pattern lists became one data file that both the shell greps
and the TypeScript scanners read, with the more permissive bound winning
wherever copies disagreed. CI installs the plugin into scratch greenfield and
brownfield repos, plants a fake credential and fails if the validator passes
it, and fails if an emptied pattern list is accepted rather than refused.
Every gate has a positive control: the defect was reintroduced and the gate was
confirmed to reject it.
Removed
The orchestration workflows are no longer copied into the target repo, where
nothing invoked them and where they could not run standalone.
customValidatorsand
modeleave the schema: a versioned config should describe the repo, notthe action being performed on it.
What still needs to be proven
The new CI jobs were simulated locally step by step, with positive controls,
but have never run on GitHub Actions. This PR is that test.
EMPIRICAL VALIDATION
Local suite, 11 checks, all green:
Compiler proof on a real repo: