· 5 min read
ECC ships a content manifest and a verify command: hash every shipped file,
compare against the manifest, report drift. Standard integrity checking.
It handles three cases, and it got two of them right.
| what changed | verdict | exit |
|---|---|---|
| a file was modified | caught | 1 |
| a file was deleted | caught | 1 |
| a file was added | advisory | 0 |
That third row was deliberate, and the code said so:
// verify is "ok" if no missing and no modified files. Extras are non-fatal.
const ok = summary.missing === 0 && summary.modified === 0;Why an added file is the attack
For most software, an extra file is noise — a stray .DS_Store, a local scratch
file. Advisory is right.
For this software it is the payload.
ECC enumerates agents/, skills/, commands/ and rules/common/ and loads
every markdown file it finds. There is no allowlist; the directory listing
is the configuration. So:
rules/common/zz-injected.mdis not an extra file sitting inertly on disk. It is a new always-on rule, loaded into every future session, with whatever instructions its author chose.
Which is exactly the payload a path traversal in our session-bundle importer delivered a few releases earlier — reached by a completely different route, and landing in the same place. When two unrelated bugs converge on one target, that target is the thing worth defending.
The human was told. Automation wasn't.
Running it by hand printed:
✓ ok: 2
✗ modified: 0
✗ missing: 0
⚠ extra: 1 (advisory)So a person reading the output would see it. But:
process.exit(report.ok ? 0 : 1);kodelyth-ecc verify && deploy passes. Any CI gate passes. The one consumer that
cannot read a warning line is the one most likely to be running this.
That gap — informative to humans, silent to machines — is worth looking for anywhere you have a tool with both a pretty printer and an exit code.
Why we didn't just change the default
The obvious fix is to make extras fatal. We didn't, and the reasoning matters more than the fix.
ECC is a toolkit people are meant to extend. Adding your own agent to
agents/ is the documented workflow. Making extras fatal means verify fails
forever for everyone who customised anything — and a check that always fails gets
ignored, which is strictly worse than a check that is sometimes too lenient.
So the choice is offered rather than made:
kodelyth-ecc verify # extras advisory, exit 0 (unchanged)
kodelyth-ecc verify --strict # extras fail the verdict, exit 1Default stays lenient for humans with customised installs. --strict is for the
deploy gate, where an unknown file in a directory that gets loaded as instructions
should stop the pipeline.
The report also now carries strict, so a --json consumer doesn't have to infer
which mode produced the verdict.
Verified through the CLI, not the library
The unit tests assert the library returns ok: false. That is necessary and not
sufficient — the exit code is what a gate actually reads, and an exit code
lives in the CLI wiring, not the function.
So the check ran end to end against the real binary:
clean default: exit 0 strict: exit 0
planted default: exit 0 strict: exit 1 ← the gateIf you are fixing something whose purpose is to fail a build, test the thing the build inspects.
The question to ask your own verifier
Does it detect additions, and does an addition change the exit code?
Most integrity tooling is built around "did my files change", which naturally covers modification and deletion. Addition needs you to know the full expected set — and then to decide, deliberately, whether an unexpected file is noise or the attack.
For anything that loads a directory as configuration, it is the attack.
Shipped in ECC 2.24.8. Manifest, verify and SBOM generation are in scripts/supply-chain.