Skip to main content
Kodelyth ECC
security

Our Integrity Verifier Passed With a Planted File

It caught modified files and deleted files. An added file was advisory — exit code 0 — and in a toolkit that loads every markdown file in a directory as instructions, an added file is the whole attack.

· 5 min read

A table. Modified file: caught, exit 1. Deleted file: caught, exit 1. Added file: advisory, exit 0 — while that file is loaded as an always-on rule.

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 changedverdictexit
a file was modifiedcaught1
a file was deletedcaught1
a file was addedadvisory0

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.md

is 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 1

Default 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 gate

If 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.