Skip to content

chore(security): gate plain-text internal domains out of state narrative files#615

Merged
SoundMindsAI merged 2 commits into
mainfrom
chore_state_domain_scrub_gate
Jul 11, 2026
Merged

chore(security): gate plain-text internal domains out of state narrative files#615
SoundMindsAI merged 2 commits into
mainfrom
chore_state_domain_scrub_gate

Conversation

@SoundMindsAI

Copy link
Copy Markdown
Owner

Summary

Implements the write-time-scrub + allowlist-gate design for keeping operator internal domains out of the public narrative files (state.md / state_history.md). No encoding anywhere — reversible encoding is not redaction; it hides data from your own scanners while leaving it one decode away for an attacker.

The threat: these files are public, free-form, and rewritten after every merge, so they're the likeliest place an internal hostname (es.prod.internal.example-inc.com), a customer domain, or a private-host IP leaks. Once pushed, that's effectively unrecallable (forks, dangling SHAs, archives).

What's in it

  • scripts/check_internal_domains.py — pure-stdlib scanner. To avoid drowning in dotted code identifiers (asyncio.gather, vi.mock), it flags a dotted token only when the final label is a real TLD (curated to exclude file extensions) or an internal pseudo-TLD (.internal/.corp/.local/…), unless the domain is allowlisted. RFC 2606/6761 reserved names (.example/.test/.invalid/.localhost) never flag — use them for fictional hosts. Also flags RFC 1918 private-host IPs, while letting universal constants through (loopback, RFC 5737 doc ranges, link-local incl. the 169.254.169.254 metadata IP that SSRF narratives need to name, and .0/.255 network/broadcast addresses).
  • scripts/allowed_public_domains.txt — allowlist of genuinely-public domains (an entry covers its subdomains). The bar to add: "is this public information?"
  • Two-layer enforcement (mirrors the existing gitleaks + .env-filename guards):
    • pre-commit hook internal-domains-guard — primary; blocks the literal from ever entering a commit object.
    • internal domains guard job in secrets-defense.yml — backstop for commits made via the GitHub web UI, --no-verify, or another machine. Lives in secrets-defense.yml (not pr.yml) so paths-ignore can't skip it.
  • docs/04_security/internal-domain-hygiene.md — the policy: placeholders (<operator-es-cluster>), the optional gitignored secrets/domain_aliases.yaml alias mapping, the no-encoding rule, the no-dot blind spot, and post-push incident guidance.
  • Write-time instruction added to CLAUDE.md's state.md section and the two skills that write state.md (impl-execute, pipeline).
  • Scrubbed the two pre-existing domain-shaped examples already in state_history.md (both were illustrative, not real infra): internal.corpinternal.example, *.soundminds.internal*.<internal-domain>.

Design decision: scrub, don't encode

Per the discussion that prompted this: encoding (base64/hex/hash/encrypt) fails the "never exposed" requirement — the data is still there, and it blinds gitleaks/this scanner too. The right model is scrub-to-placeholder at write time, keep the real-name mapping in a gitignored file, enforce with an allowlist gate. This PR is exactly that.

Known blind spot (documented, by design)

A bare hostname with no dot (es-prod-01) is indistinguishable from a word and can't be pattern-detected. That's what the write-time placeholder discipline (CLAUDE.md + skills) is for — the gate is the backstop, not the whole defense.

Testing

15 unit tests pin both edges (must-flag: internal FQDN / customer domain / subdomain / .corp; must-not-flag: allowlisted / subdomain-of-allowlisted / suffix-not-substring / reserved / dotted-code-identifier / angle-bracket placeholder; IP classification incl. universal-constant exemptions). A regression test asserts the committed state.md + state_history.md stay clean. Full backend/tests/unit/scripts/ suite green (314); all pre-commit hooks pass.

Note on already-pushed content

The two scrubbed examples remain in past commit objects (history), but they're illustrative not real infrastructure, so no history surgery is warranted. If a genuinely-sensitive operator domain is ever found already-pushed, the policy doc's incident section covers it.

🤖 Generated with Claude Code

…ive files

state.md / state_history.md are public, free-form, and written after every
merge — the highest-risk place for an operator's internal hostnames to leak
into a public repo. Add a scrub-at-write-time + allowlist-gate defense
(no encoding — reversible encoding is not redaction):

- scripts/check_internal_domains.py — pure-stdlib scanner. Flags a dotted
  token only when its final label is a real TLD (curated to exclude file
  extensions) or an internal pseudo-TLD (.internal/.corp/.local/...), unless
  covered by the allowlist; RFC 2606/6761 reserved names (.example/.test)
  never flag. Also flags RFC 1918 private-host IPs while letting universal
  constants through (loopback, RFC 5737 doc ranges, link-local incl. the
  169.254.169.254 metadata IP, network/broadcast addresses).
- scripts/allowed_public_domains.txt — allowlist of genuinely-public domains
  (subdomain-covering); the bar to add is "is this public info?".
- Two-layer enforcement mirroring the gitleaks/env-file guards: pre-commit
  hook `internal-domains-guard` (primary — blocks entry into any commit
  object) + `internal domains guard` job in secrets-defense.yml (backstop for
  web-UI / --no-verify / other-machine commits; outside pr.yml's paths-ignore).
- docs/04_security/internal-domain-hygiene.md — the policy (placeholders,
  optional gitignored secrets/domain_aliases.yaml mapping, no-encoding rule,
  the no-dot blind spot, post-push incident guidance).
- Placeholder rule added to CLAUDE.md's state.md section + the two skills that
  write state.md (impl-execute, pipeline).
- Scrubbed the two pre-existing domain-shaped examples in state_history.md to
  guard-clean forms (internal.corp -> internal.example; *.<internal-domain>).

15 unit tests pin both edges (must-flag real leak shapes; must-not-flag
allowlisted/subdomain/reserved/code-identifier/placeholder). The committed
state files pass the gate.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: SoundMindsAI <eric.starr@soundminds.ai>

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request establishes an internal-domain hygiene policy to prevent plain-text operator domains, sub-domains, and private-host IPs from leaking into public narrative files like state.md and state_history.md. It introduces a Python scanner (check_internal_domains.py), an allowlist of public domains, comprehensive unit tests, and a pre-commit hook (internal-domains-guard) mirrored in CI. Existing narrative files have also been scrubbed of internal domains. The review feedback correctly identifies broken relative links to the new security documentation in the skill markdown files, which need to be adjusted to go up three directory levels instead of two.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread .claude/skills/impl-execute/SKILL.md Outdated
- Add the feature to `## Most recent meaningful changes` with a summary paragraph (component changes, test counts, key decisions).
- Update `**Current focus:**` line to include the feature as merged.
- Update `## Current branch / execution context` to reflect the new state.
- **Never write a plain-text operator domain, sub-domain, or private-host IP** into `state.md` / `state_history.md` — these are public files. Use a human-readable placeholder (`<operator-es-cluster>`, `<corp-proxy>`, `<internal-host>`) or an RFC-reserved example name (`foo.example`). No encoding. The `internal-domains-guard` pre-commit hook + CI job will block the commit otherwise. See [`docs/04_security/internal-domain-hygiene.md`](../../docs/04_security/internal-domain-hygiene.md).

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The relative link to internal-domain-hygiene.md is broken. Since this file is located at .claude/skills/impl-execute/SKILL.md (which is 3 levels deep from the repository root), the relative path needs to go up three levels (../../../) instead of two (../../) to correctly resolve to the root-level docs/ directory.

Comment thread .claude/skills/pipeline/SKILL.md Outdated
- Update `pipeline_status.md` to `Implementation: Complete`
- Update `implementation_plan.md` status to `Complete (PR #<N>)`
- Update `state.md`: add to recent changes, update current focus + branch context
- Update `state.md`: add to recent changes, update current focus + branch context. **Never write a plain-text operator domain / sub-domain / private-host IP** — use a placeholder (`<operator-es-cluster>`, `<corp-proxy>`) or RFC-reserved example name; the `internal-domains-guard` hook + CI will block the commit otherwise (see `docs/04_security/internal-domain-hygiene.md`).

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The relative link to internal-domain-hygiene.md is broken. Since this file is located at .claude/skills/pipeline/SKILL.md (which is 3 levels deep from the repository root), the relative path needs to go up three levels (../../../) instead of using a direct relative path without any leading ../ segments.

…emini)

- Fix relative-link depth in the two skill files: SKILL.md files are 3 levels
  deep, so links to docs/ need ../../../ not ../.. (Gemini review). Also fixed
  a pre-existing broken data-model.md link in impl-execute SKILL.md, same class.
- Remove the maintainer org apex (soundminds.ai) from the domain allowlist: it
  had 0 occurrences in the state files and allowlisting an apex passes ALL its
  subdomains, so an internal host under it (ci.<org>) would slip the gate.
  Keep relyloop.com (public product site, 7 real occurrences). Document this
  allowlisted-apex-subdomain residual in the policy doc.

Gate still passes on the committed state files; 15 guard tests green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: SoundMindsAI <eric.starr@soundminds.ai>
@SoundMindsAI

Copy link
Copy Markdown
Owner Author

Review adjudication (pre-merge)

Gemini Code Assist — 2 findings, both accepted + fixed in 483a42b1:

Finding Verdict Action
impl-execute/SKILL.md:871 — relative link to internal-domain-hygiene.md uses ../.. but the file is 3 levels deep Accept Fixed to ../../../. Also fixed a pre-existing broken data-model.md link in the same file (same class).
pipeline/SKILL.md:280 — doc reference not a resolvable link Accept Upgraded the bare backtick path to a proper ../../../docs/... markdown link matching the file's convention.

Self-review — one additional tightening (not from Gemini):

  • Removed the maintainer org apex soundminds.ai from the allowlist: it had 0 occurrences in the state files, and allowlisting an apex passes all its sub-domains, so an internal host under it (ci.<org>) would have slipped the gate. Kept relyloop.com (public product site, 7 real occurrences). Documented the allowlisted-apex-subdomain residual as a known blind spot in the policy doc.

All doc links verified to resolve; gate passes on the committed state files; 15 guard tests green.

Pre-existing red check (NOT this PR): backend (contract + integration) — the same test_synthetic_ubi_seed_round_trip_hits_rung_3 UBI test documented in #610 (bug_ubi_fast_test_classifies_rung_1_in_ci); untouched by this diff. Merge-skew: clean.

@SoundMindsAI
SoundMindsAI merged commit 2c79042 into main Jul 11, 2026
17 of 20 checks passed
@SoundMindsAI
SoundMindsAI deleted the chore_state_domain_scrub_gate branch July 11, 2026 11:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant