The forbidden-string gate: badcode

badcode is a release-blocking scan for text that must never reach a release under any circumstances -- names, internal codenames, credentials-shaped tokens, "DO NOT SHIP" markers. relcore (see repoman-070-releases.md) runs it automatically and unconditionally as the very first thing it does, on every release, including on --resume -- not a step that can be skipped or configured away.

The design constraint that shapes everything here

The pattern list is deliberately never stored in any repository. It lives in a local, per-machine config directory instead, because a blocklist that ships inside the repo it's protecting can be edited by whoever has commit access -- including an overly-permissive or compromised agent -- in the same change that would have been caught by it. Keeping it local and out-of-band is the entire point: the check exists outside the reach of "just edit the config to stop being blocked."

This has a direct consequence worth stating plainly: a fresh session in a fresh sandbox has no badcode config until one is recreated. Nothing is wrong if badcode check reports zero patterns configured -- that's the expected starting state, not a broken installation. See "No config configured" below for what that actually looks like and why it's a soft pass, not a silent one.

Config location and format

The OS-appropriate user config directory (via os.UserConfigDir() -- ~/.config on Linux, ~/Library/Application Support on macOS, %AppData% on Windows) under a repoman subdirectory, overridable via the REPOMAN_BADCODE_DIR environment variable. Two file formats are read, both optional, both additive if both exist:

badcode.txt    one pattern per line; blank lines and lines starting
                with '#' are ignored.
badcode.json   [{"pattern": "...", "reason": "..."}, ...] -- "reason"
                is optional, and included in the refusal message when
                a pattern matches, so a match is actionable rather
                than just a bare string.

Matching: literal substring, not regex -- deliberately

Case-insensitive, literal substring search. Not regex. Regex adds failure modes (a pattern that silently doesn't compile, ReDoS, an off-by-one in an anchor) to a check whose entire value is that it never has a false "clean" result -- a plain substring search can't fail in a way that looks like success.

This does mean a pattern split across an ordinary line wrap is checked for too, not just a pattern sitting entirely on one line -- an accidental line wrap is enough to split a forbidden name across two lines by complete accident, not just deliberate evasion, and a match found only that way is reported with a [spans lines N-M] marker so the message stays precise about what actually happened.

Binary files are skipped (the same NUL-byte heuristic git itself uses), so a forbidden byte sequence that happens to occur coincidentally inside a compiled artifact is never flagged.

Usage

$ repoman badcode check .
BADCODE CHECK OK (4 pattern(s) checked)

A real match refuses clearly, with file, line, provenance status, and (if configured) the reason:

$ repoman badcode check .
BADCODE CHECK FAIL: 1 match(es)
ERROR badcode-match: pattern "INTERNAL_CODENAME" (project codename --
must never appear in anything meant for external publication) found
in README.md:12 [no provenance record]: still using INTERNAL_CODENAME
in this draft

The bracketed provenance status (T-04) is one of three labels: repoman (the matched file's current content matches repoman's own journal record -- it genuinely was written through ed/strreplace), no provenance record (the file has never been touched by repoman at all), or stale provenance record (repoman wrote the file at some point, but it has since changed outside repoman's own write paths too). No adjacent secret-scanning tool (Gitleaks, TruffleHog, and similar) can make this distinction, since none of them own the editing layer the way repoman does.

No config configured

$ repoman badcode check .
WARN no badcode patterns configured in /home/user/.config/repoman -- nothing checked
BADCODE CHECK OK (0 patterns configured)

Exit 0 -- optional, local, per-operator tooling shouldn't block every release for an operator who hasn't set it up yet. But the WARN line is always there, on every invocation with no config, so a check that never actually ran can't be mistaken for a check that ran clean. This is the same guard principle repoman-020-failure-modes.md describes for dormant guards generally: a check that never actually ran proves nothing, and this one says so plainly rather than looking identical to a real pass.

Recreating the config in a fresh session

Because the config is deliberately never committed, a fresh session starts with none. If the project has one configured (check prior session notes, or ask), recreate it at the start of any session doing release-related work, before that work begins -- not discovered as missing partway through a release:

mkdir -p ~/.config/repoman
cat > ~/.config/repoman/badcode.json << 'EOF'
[
  {"pattern": "...", "reason": "..."}
]
EOF

Integration with relcore

relcore <version> runs badcode check against the whole tree as the first thing it does, before reading release.steps, before any step runs, including on --resume -- it is not part of the resumable-steps journal at all, so there is nothing for --resume to bypass. A real match blocks the entire release; fixing the match (removing the matched content) and re-running proceeds normally.

Immediately alongside it, relcore also runs a provenance pre-flight (T-03, see repoman-088-provenance.md) with the exact same discipline -- unconditional, not a release.steps entry, not resumable, no bypass on --resume. An unsanctioned out-of-band edit to a journal-tracked file blocks the release the same way a badcode hit does; repoman provenance sanction FILE --reason "..." is the only way through. See repoman-070-releases.md for the full release workflow this sits inside.