Attribution markers become a single provider-declared pattern set, boot-validated and unioned across every registered provider, enforced identically by the pre-push hook and the body sanitizer¶
Incogni (ADR-0012, D15) has two enforcement points against AI-attribution
leaks: the pre-push guard in the bare reference repo (measure 7) and the
agent-API body sanitizer (measure 3). Issue #51 decision 8 made the hook
provider-declared — seeder.InstallPrePushHook renders its grep -i scan
from the resolving provider's SeedMeta.ScrubPatterns/SeededPathPatterns
— but the sanitizer (sanitizeBody → attributionLine, internal/agentapi)
stayed a hardcoded claude shape in core: a Co-Authored-By: Claude… trailer
and a bare @anthropic.com mention, baked into ADR-0026's otherwise
provider-neutral seam. Triage of #2 (Codex) exposed why that's a bug, not a
cosmetic gap: a Co-authored-by: ChatGPT <noreply@openai.com> footer sails
straight through attributionLine untouched even on an incogni repo,
because core knows only Anthropic's shape.
A second gap sits underneath the first. incogniSeedMeta
(internal/reposvc) resolves the hook's patterns from the repo's effective
default provider only (repos.provider → provider_default skip-layer,
per ADR-0030) — the provider a repo is stamped with, not the provider a
given session actually ran. ADR-0030 made the agent CLI a per-spawn
override: any registered provider can run against any repo for one session.
A repo defaulted to claude-code but spawned once against Codex would push
through a hook still scrubbing only claude's markers, and the sanitizer had
no session context to consult at all — it ran off repo.Incogni alone.
Both gaps are closed the same way: attribution markers are a declared
provider property, and every registered provider's declaration must be
consulted at both enforcement points, unconditionally.
Decisions, pinned:
- One declaration, two enforcement points.
SeedMeta.ScrubPatternsfeeds BOTH the pre-push hook (grep -iPOSIX BREs insh, unchanged) and the body sanitizer, where each pattern is compiled once as a case-insensitive Go regexp (a(?i)prefix) and applied per line. This binds the two mechanisms to the common BRE∩RE2 dialect — claude-code's four patterns already live there ([[:space:]],[^>]*parse under both engines), so no pattern rewrite is needed today. No new interface method is added toAgentProvider: a provider declares its markers, core owns both mechanisms that act on the declaration — the seam-v2 idiom (ADR-0026), not a capability per mechanism. A future provider whose attribution isn't line-shaped is the case the ADR-0017 optional-capability escape hatch exists for; it is not built speculatively here. - Boot-time validation.
provider.NewRegistrycompiles every registered provider'sScrubPatternsas Go regexps and rejects construction if any fail — an unenforceable marker set is a wiring bug, caught at boot next to the existing empty-id/duplicate-id checks (internal/provider/registry.go), never a silent enforcement gap discovered in production. The compiled union is cached on the registry (ScrubRegexps) so neither enforcement point recompiles per call. - Union across all registered providers, at both enforcement points.
The hook's scrub patterns AND seeded-path patterns become the union over
every provider in the registry (registration order, exact-string dedup)
—
incogniSeedMeta's per-repo effective-provider resolution is deleted in favor ofregistry.List(). Rationale: since ADR-0030, any registered provider can run on any repo via the per-spawn override, so a guard keyed to one repo's default provider is exactly the race described above — the guard must screen every registered provider's markers regardless of which one the pushing session ran. Over-matching is safe: ADR-0012 already pins each provider'sSeededPathPatternsto a deliberately narrow list — only what lab itself seeds for that provider — so the union of narrow lists stays narrow; a repo is over-rejected only for paths lab would seed for some registered provider, which is acceptable and symmetric with today's single-provider behavior. Side effect: the hook install path no longer readsrepos.provideror theprovider_defaultsetting at all; an empty registry now yields a content-inert guard (both pattern lists empty, matching seeder.InstallPrePushHook's existing empty-list handling) instead ofincogniSeedMeta's current "no agent providers registered" error — consistent with the rest of the codebase's nil-registry degraded boot. - Sanitizer pattern-set reconciliation.
attributionLinepreviously stripped aco-authored-by:line carrying a bareanthropic.commention anywhere after the prefix; claude-code's declared pattern requires the bracketed email (co-authored-by:.*<[^>]*@anthropic\.com>) — ADR-0012's own review pinned the bracketed email, not the bare domain or the "Claude" display name, as the stable discriminator. The declared set is adopted unchanged (the hook's golden output stays byte-identical); the sanitizer's coverage narrows to match the hook's shape, pinned by a test case asserting an unbracketedanthropic.commention is now kept. The sanitizer's seam-repair mechanics — blank-line collapse at a removal seam, code-fence blank runs left untouched, byte-identity for non-incogni bodies, running afterensureCloses— are unchanged; only the per-line predicate moves fromattributionLine's hardcoded claude cases to the registry's declared union. - Core neutrality made testable. A Go arch test in internal/provider
scans non-test core string literals for attribution-marker tokens
(
anthropic,claude-session,co-authored-by,generated with), exempting internal/provider/claudecode and internal/provider/providertest (where the tokens are the declaration and its fakes, not a leak) and every_test.go/testdata file (fixture literals). This is the Go twin of the SPA'sproviderNeutral.test.tsgrep-guard (ADR-0026). The exemption is deliberately narrow: a bareclaudeliteral stays legitimate in core for CLI/config UX unrelated to attribution — flag names,~/.claude.jsonpaths — only the attribution-marker token set is forbidden outside the two exempted packages. There is no allowlist for a core hit that matches anyway; it must be refactored into provider-declared metadata instead.
Status¶
Accepted. Amends ADR-0012 (D15 measures 3 and 7 both now read from one
provider-declared, boot-validated, registry-wide union instead of a
core-hardcoded sanitizer and a per-repo-resolved hook) and issue #51
decision 8 (SeedMeta.ScrubPatterns gains a second consumer). Applies
ADR-0026's seam-v2 idiom (provider declares, core owns the mechanism) and
keeps ADR-0017's optional-capability pattern as the reserved escape hatch
for a future non-line-shaped attribution scheme. Closes the ADR-0030 race
between a per-session provider override and a guard keyed to one repo's
default provider. No new AgentProvider method, no schema change.
Considered options¶
- Re-render the hook per session with the session's own provider, and
keep the sanitizer as-is. Rejected: still misses the race between two
concurrent sessions of different providers pushing through the same bare
repo at once, adds churn to a file rewritten on every spawn, and the
sanitizer has no session context available to it at all — the agent API
only ever sees
repo.Incogni, not which provider pushed which run. - A new interface method or capability for body sanitizing, one per provider. Rejected: no second mechanism exists that would justify a provider-owned sanitizer — declaration plus one core-owned mechanism (now two: hook and sanitizer) is exactly the seam-v2 idiom ADR-0026 already established, and ADR-0017's escape hatch remains available if a provider ever needs to override the declarative path.
- Separate pattern languages per enforcement point — two declarations, one for the hook, one for the sanitizer. Rejected: drift between the two sets is precisely the bug this ADR fixes (claude-code's sanitizer shape had already drifted from its own hook's declared pattern). One declaration, compiled twice, cannot drift.
- Widen the declared pattern to match the sanitizer's old bare-domain
breadth (
anthropic\.comunbracketed). Rejected: it would re-pin the hook's golden output for no real-world shape — git trailers carry bracketed emails, not bare domain mentions — and ADR-0012's review already chose the bracketed email as the durable discriminator over a model/display-name string.
Consequences¶
- internal/provider/registry.go:
NewRegistrygains regexp-compilation validation and the cachedScrubRegexpsunion;Registrygains a way to answer the unioned scrub/seeded-path pattern sets by iteratingList(). - internal/reposvc/reposvc.go:
incogniSeedMetaandinstallIncogniHookstop resolving a single effective provider; the hook is rendered from the registry's union. An empty registry degrades to a content-inert guard rather than erroring. - internal/agentapi/handlers.go:
attributionLineis replaced by a predicate driven by the registry's compiledScrubRegexps;sanitizeBodyis otherwise unchanged. A previously-passing bare-anthropic.comtest case is repinned to assert the line is now kept. - internal/provider gains an arch test guarding core neutrality for
attribution-marker tokens, alongside the existing seam-neutrality
discipline (no concrete provider import in httpapi, ADR-0026). Its first
run caught the incogni seed-prompt sentence (internal/afk/decide.go,
measure 2) spelling
Co-Authored-Byliterally; the sentence is reworded to name the shapes generically ("no co-author trailers, no tool-credit footers, no session links") so core prose carries no marker token either. - Registering #2 (Codex) with its own
ScrubPatterns(e.g. aChatGPT/@openai.comtrailer shape) requires no core change: both enforcement points pick it up automatically as soon as it joins the registry.