@@ -90,18 +90,11 @@ if os.path.isfile(config_path):
9090if enforce is not True or gate_off:
9191 emit("ALLOW")
9292
93- # The rule is "no credentials hardcoded into SOURCE". Config-shaped homes for
94- # secrets, and files whose whole point is placeholder values, are allowed.
9593rel = os.path.relpath(
9694 file_path if os.path.isabs(file_path)
9795 else os.path.join(project_root, file_path), project_root).replace("\\", "/")
9896base = os.path.basename(rel)
99- if base.startswith(".env") or base.endswith((".md", ".txt", ".lock", ".pem.example")):
100- emit("ALLOW")
10197parts = rel.split("/")
102- if any(p in ("examples", "fixtures", "tests", "test", ".sage", "sage",
103- ".claude", "node_modules") for p in parts):
104- emit("ALLOW")
10598
10699# New content: Write carries `content`; Edit carries `new_string`; MultiEdit a
107100# list of edits. Concatenate whatever is present.
@@ -117,8 +110,41 @@ text = "\n".join(blobs)
117110if not text:
118111 emit("ALLOW")
119112
120- # Provider-shaped tokens. Precision over recall: every pattern anchors on a
121- # vendor prefix, so a random identifier cannot trip it.
113+
114+ def block(what):
115+ emit("BLOCK", (
116+ "sage-secrets-gate: this edit hardcodes %s into %s — credentials "
117+ "never go into files (constitution: secrets).\n"
118+ "\n"
119+ "Instead: read it from the environment (os.environ / process.env) "
120+ "or a gitignored config (.env), and reference the variable here. "
121+ "If a placeholder is genuinely needed, use an obvious fake like "
122+ "\"YOUR_API_KEY\"." % (what, rel)))
123+
124+
125+ # ── Class 1: LIVE-marked keys — blocked EVERYWHERE except .env*. ──
126+ # The weak-model proof run caught the gap: E2's key (pfk_live_…) is a fictional
127+ # vendor prefix no provider list can anticipate, and one run parked it in
128+ # tests/ — which the source-only class exempts for FAKE fixtures. A key that
129+ # says live/prod/secret in its own name is not a fixture: `live` means live,
130+ # and it belongs in .env or nowhere. (sk_test_-style keys stay in class 2 —
131+ # vendors design those for code and CI.)
132+ if not (base.startswith(".env") or base == ".gitignore"):
133+ m = re.search(r"\b[A-Za-z]{2,8}_(?:live|prod|secret)_[A-Za-z0-9]{12,}", text)
134+ if m:
135+ block("a live-marked key (%s…)" % m.group(0)[:12])
136+ if re.search(r"-----BEGIN [A-Z ]*PRIVATE KEY-----", text):
137+ block("a private key block")
138+
139+ # ── Class 2: provider-shaped tokens — blocked in SOURCE only. ──
140+ # Placeholder-shaped fakes are legitimate in tests/fixtures/examples and docs;
141+ # a guard with false positives is a guard people disable.
142+ if base.startswith(".env") or base.endswith((".md", ".txt", ".lock", ".pem.example")):
143+ emit("ALLOW")
144+ if any(p in ("examples", "fixtures", "tests", "test", ".sage", "sage",
145+ ".claude", "node_modules") for p in parts):
146+ emit("ALLOW")
147+
122148PATTERNS = [
123149 (r"\bsk-[A-Za-z0-9_-]{16,}", "an sk-… API key"),
124150 (r"\bsk-ant-[A-Za-z0-9_-]{16,}", "an Anthropic API key"),
@@ -127,18 +153,10 @@ PATTERNS = [
127153 (r"\bgithub_pat_[A-Za-z0-9_]{20,}", "a GitHub fine-grained token"),
128154 (r"\bxox[baprs]-[A-Za-z0-9-]{10,}", "a Slack token"),
129155 (r"\bAIza[0-9A-Za-z_-]{30,}", "a Google API key"),
130- (r"-----BEGIN [A-Z ]*PRIVATE KEY-----", "a private key block"),
131156]
132157for pat, what in PATTERNS:
133158 if re.search(pat, text):
134- emit("BLOCK", (
135- "sage-secrets-gate: this edit hardcodes %s into %s — credentials "
136- "never go into source (constitution: secrets).\n"
137- "\n"
138- "Instead: read it from the environment (os.environ / process.env) "
139- "or a gitignored config (.env), and reference the variable here. "
140- "If a placeholder is genuinely needed, use an obvious fake like "
141- "\"YOUR_API_KEY\"." % (what, rel)))
159+ block(what)
142160
143161emit("ALLOW")
144162PYEOF
0 commit comments