Development record
mergeDomain replaces a domain's recall and rules arrays wholesale, so a repo that overrides either array silently withholds every later security upgrade to the bundled defaults: internal/core/rules/rules.go copies the override array over the base one instead of unioning, with no schema bump and no notice, so a repo that pinned PII recall or PII rules before iss-156 keeps the old set and never sees the new network keywords or the never-commit-network-identifiers rule line. Per-field merge is the documented and wanted behaviour for state, but for security-bearing arrays the quiet outcome is a stale ruleset that looks current. Options to weigh: union rather than replace for recall/aliases (additive, no loss), a distinct replace-vs-extend marker in the override, or at minimum a loud diagnostic in abcd rules naming which bundled entries an override is withholding. First activated by the iss-156 PII upgrade, which is why it is captured now. Prior disposition to weigh when triaging: iss-66 (resolved, rules-loader trust boundary) document-accepted the adjacent risk that an override can weaken a default guardrail domain (its P15); this entry is the distinct case of an override silently withholding later upgrades rather than deliberately silencing a domain.
mergeDomain replaces a domain's recall and rules arrays wholesale, so a repo that overrides either array silently withholds every later security upgrade to the bundled defaults: internal/core/rules/rules.go copies the override array over the base one instead of unioning, with no schema bump and no notice, so a repo that pinned PII recall or PII rules before iss-156 keeps the old set and never sees the new network keywords or the never-commit-network-identifiers rule line. Per-field merge is the documented and wanted behaviour for state, but for security-bearing arrays the quiet outcome is a stale ruleset that looks current. Options to weigh: union rather than replace for recall/aliases (additive, no loss), a distinct replace-vs-extend marker in the override, or at minimum a loud diagnostic in abcd rules naming which bundled entries an override is withholding. First activated by the iss-156 PII upgrade, which is why it is captured now. Prior disposition to weigh when triaging: iss-66 (resolved, rules-loader trust boundary) document-accepted the adjacent risk that an override can weaken a default guardrail domain (its P15); this entry is the distinct case of an override silently withholding later upgrades rather than deliberately silencing a domain.
Grounds
- pursued: a repo or user override that holds back a bundled PII, COMMITTING or LOAD entry is told which entry and which file on every load; an override withholding one with no stderr note, or a note reaching the injected context or the --json document, would show it wrong
Frontmatter
| id | iss-174 |
|---|---|
| type | issue |
| lifecycle | resolved |
| severity | minor |
.abcd/work/issues/resolved/iss-174-rules-override-withholds-bundled-default.md
open on GitHub ↗ · commit history ↗
mentions5
- dcn-26081500000000022026-08-15 — itd-84 (intent decomposition) ADOPTED at the MVP rung, per its own staged promotion ladder: the `decompose-before-filing` principle is filed, the hand-run four-piece protocol lives in the /abcd:intent surface page (capture and the planning interview both run it, loud-staged as not-yet-automated), a DECOMPOSITION rules domain injects the rule at recall time, and every hand-run is graded into `.abcd/development/research/notes/2026-08-15-decomposition-calibration.md` (the 2026-07-13 auto-merge SPLIT case seeds the corpus retrospectively). The deterministic Go pre-pass is admitted to the predictable-development plan's queue (front-door design open: `decompose` subverb vs a check in `intent ready`; collision with iss-210 noted). The capture-time agent rung stays DEFERRED until ~50 graded captures exist (itd-81 calibration); building or running it earlier is a recorded STOP. Rejected: overriding the bundled INTENTS rules domain to carry the new rule (a per-field override replaces the whole array and would withhold future bundled upgrades — iss-174's exact failure mode), hence the additive custom domain instead.
- dcn-26092600000000062026-09-26 — Three of the rules loader's security records close, and one stays owed to the product thinker (autonomous run A orchestrator's lane brief, taken by the implementer of lane drainS2). (1) The home directory is never a session's repo root (iss-2609020219198779, answering the owed question "is a home-directory git toplevel a legitimate config scope, or excluded outright?" as the brief rules it): its `.abcd/` is the user layer, so the root walk passes over the home and a toplevel that is the home resolves like a non-repo directory. The lane narrowed the brief's "or an ancestor of HOME": a toplevel that contains the home, the shape of a hermetic harness that points `HOME` inside its checkout, stays the root because git vouched for it and its own `.abcd/` is its own; only the stop at the home is removed. A session whose working directory is the home still reads a `.abcd/` there as the working directory's, the posture question recorded on 2026-09-25. (2) A bundled guardrail that an override withholds is named on every load (iss-174): for COMMITTING, LOAD and PII, each bundled recall keyword, alias or rule missing from a list an override set goes to stderr with the file whose list is in force, and the merge stays per field. The other bundled domains are left out because a repository restates them in its own words, and a note on every restatement would bury the one that matters. Still owed to the product thinker: whether security-bearing lists should union with the bundled entries or take a replace-versus-extend marker instead (itd-117's finer-grained-merging follow-up). (3) The foreign-uid refusal says what it still reads (iss-2609251522588539): the note, the configuration chapter and the install how-to now say that a `.abcd/` at the working directory is read, as AGENTS.md has since 0434d475. (4) iss-2609020219265817 is deferred past v0.11.0, not closed. Every CommonMark heading construct in a rule body (ATX on any line, the first line included, setext, and HTML h1-h6) can be closed only by a code-safe rendering that flattens legitimate structure, or by a fence-aware escaper that is complete only by enumeration and changes the raw text the model reads. So "escaped, fenced, or left to the line-start contract" is the product thinker's ruling.
- iss-66rules loader trust-boundary (itd-3): an untrusted per-repo .abcd/rules.json can silence default guardrail domains — Merge() ORs the global kill switch (rules.go:156) and mergeDomain lets an override set any default domain dormant, so a cloned repo suppresses all rule injection (P15); session-state lives in a predictable shared /tmp path (os.TempDir/abcd-rules-state, sha256(session).json, session defaults to constant default) letting a local co-tenant suppress injection fail-open (inject.go:99, P14); Load() Lstats rules.json then os.ReadFile re-resolves by name, a symlink-swap TOCTOU (rules.go:132, C19). Detector: override cannot disable a default guardrail/kill-switch; state under UserCacheDir+ownership check; open-once O_NOFOLLOW+fstat. Corpus: P15, P14, C19.
- iss-156PII rules domain cannot fire on the incident that most needs it: recall keywords (secret, token, credential, pii, redact, hostname, email) miss network/infra contexts (tailscale, vpn, ip, firewall, network, reachability), and the injected rule text carries no never-commit-hostnames-IPs-live-data rule at all — that rule exists only in the parent CLAUDE.md privacy section. An agent writing up a Tailscale investigation gets zero PII injection and zero relevant rule text even if it did.
- iss-2609012039215212GHSA-22f8-qf5r-gjgq (CWE-451, CWE-1427): a `.abcd/rules.json` override replaces a bundled domain's rules wholesale (`mergeDomain`, internal/core/rules/rules.go) and nothing downstream records where the words came from — `ResolvedDomain` carries no origin, `renderDomain` emits name and bullets only, the prompt-router diagnostic prints names, and `abcd rules --json` marshals the same origin-less shape — so a hostile or compromised checkout injects attacker-chosen text under `## PII` and the tool's own `# abcd rules` header, byte-indistinguishable from a bundled default in every consumer-visible surface. Reproduced at v0.7.0. The fix must derive provenance at merge time with no rules.json schema change: a `source` field on `ResolvedDomain` valued `bundled` or `repo` (leaving room for `user`, spc-23 AC6), the rendered heading `## NAME (repo override)` for a repo-sourced domain so the marker lands in the agent's context, in `abcd rules` and inside the dedup signature, the source per injected name on the hook's stderr diagnostic, and the field in `rules --json`. The marker moves `Signature` for overridden domains only: a one-time re-injection on the first post-upgrade prompt. Named as out of scope: the guard registry merge (internal/core/guard/config.go) has the same provenance-less shape for `.abcd/guard.json` overrides, but its surface is a block/allow decision, not injected prose. Siblings captured separately: `"rules": []` injecting a heading-only block, and a stop-word recall keyword firing on ordinary prose. iss-174 (an override withholds bundled upgrades) stays open and distinct. Advisory severity medium.