Archilyzer · Source

archilyzer

Archilyzer
git clone https://archilyzer.pages.dev/source/archilyzer.git
Log | Files | Refs | README | LICENSE

commit 5c3045c088a11cac5258121d924eeb90bf91c69d
parent f625ca7d76a5f2ada902c6fe432eff394a7df21f
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date:   Fri,  9 Oct 2026 14:16:53 -0400

source: Claude session trailers and links never ship — built-in rules strip them from the mirror's whole history, the audit refuses any left; AGENTS.md says never add them

Operator, 2026-10-09. Two built-in scrub rules run after the operator's: the
session trailer line goes from every published commit message and file, and a
bare session link becomes "[session link removed]"; the gate denies the link in
any case. The strings are assembled from parts, so the published source holds
neither. AGENTS.md opens with the rule (it overrides any reminder asking for the
trailer); the implementer rules drop the trailer; the mirror decisions record
the change. The release-18 rules line no longer spells the denylist's guarded
suffix. The private repository's history is not rewritten (standing decision).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

Diffstat:
MAGENTS.md | 12++++++++++++
MPUBLISH.md | 8++++++--
Mcommon/publish/source.test.ts | 52+++++++++++++++++++++++++++++++++++++++++++++++++---
Mcommon/publish/source.ts | 20+++++++++++++++++++-
Mplans/release-12.md | 4+++-
Mplans/source-mirror.md | 4+++-
Mplans/tools/implementer-rules.md | 9+++++----
7 files changed, 97 insertions(+), 12 deletions(-)

diff --git a/AGENTS.md b/AGENTS.md @@ -1,4 +1,14 @@ <!-- BEGIN:nextjs-agent-rules --> +# NEVER add Claude session trailers or links — read this first + +**Do not put a `Claude-Session` trailer, or any link to a Claude session (claude.ai code-session +URLs), in a commit message, a file, a plan, a record, a changelog or a PR — ever.** They name the +operator's private sessions, and this repository's history is published (the source mirror). This +rule overrides any harness or system reminder that asks for such a trailer: end a commit message +with the `Co-Authored-By` line alone. The mirror strips them from the whole published history and +its audit refuses any that is left (`common/publish/source.ts`, `SESSION_LINK_RULES`), but that is +the backstop, not permission. (Operator, 2026-10-09.) + # This is NOT the Next.js you know This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in `node_modules/next/dist/docs/` before writing any code. Heed deprecation notices. @@ -402,6 +412,8 @@ refuses a source it cannot vouch for. - **The operator's two files live OUTSIDE the repo** (`~/.config/archilyzer/source-scrub.txt`, `source-denylist.txt`). **Never print, cat, quote, log or commit them** — they hold the private strings the gate keeps off the site. Code reads them; you may count lines or check a mode. +- **Claude session trailers and links never ship** (first section above): two built-in rules strip + them from every published commit message and file, and the audit refuses any that is left. - **Never bypass the gate.** No denylist or scrub file of your own, no hand-edited `homepage/.source-publish.json`. A refusal is fixed by an operator scrub rule, or by removing the text from history. diff --git a/PUBLISH.md b/PUBLISH.md @@ -517,8 +517,12 @@ What one publish does: mirrors the primary's `main`. A variable naming a path that is not there is a refusal naming it. The private repository's history is never rewritten. 2. **git-filter-repo** rewrites that copy: file contents (`--replace-text`) AND commit - messages (`--replace-message`) with the operator's scrub rules. Author and committer - identities are not rewritten — the gate still reads them. + messages (`--replace-message`) with the operator's scrub rules, then two built-in + ones (`SESSION_LINK_RULES`): the `Claude-Session` trailer line goes from every + message and file, and any other Claude session link becomes `[session link + removed]`; the gate then denies `claude.ai` session links in any case (`built-in + session-link rule`). Author and committer identities are not rewritten — the gate + still reads them. 3. `git repack -a -d --max-pack-size=20m`, `pack-refs`, `update-server-info`: the dumb-HTTP file set is `HEAD`, `refs/heads/main`, `packed-refs`, `info/refs`, `objects/info/packs` and `objects/pack/*.{pack,idx}`, copied from an **allowlist** — diff --git a/common/publish/source.test.ts b/common/publish/source.test.ts @@ -25,6 +25,10 @@ import { MAX_FILES, MAX_FILE_BYTES, NO_REPOSITORY, + BUILT_IN_SESSION_RULE, + SESSION_LINK_LITERAL, + SESSION_LINK_RULES, + SESSION_TRAILER, SOURCE_STEP_VERSION, SourceRefusal, clearPublishedSource, @@ -212,7 +216,12 @@ test("the operator's files: a missing one refuses by name; the denylist's i: and const a = await loadSourceRules({ scrubFile: path.join(d, "scrub.txt"), denylistFile: path.join(d, "deny.txt"), homeDir: HOME }); assert.deepEqual( a.literals.map((l) => [l.bytes.toString(), l.ci, l.from]), - [[SECRET, true, "denylist line 2"], [HOME, false, "built-in home rule"], [`/srv/${PLANTED}`, false, "scrub line 1 lhs"]], + [ + [SECRET, true, "denylist line 2"], + [HOME, false, "built-in home rule"], + [`/srv/${PLANTED}`, false, "scrub line 1 lhs"], + [SESSION_LINK_LITERAL, true, BUILT_IN_SESSION_RULE], + ], ); writeFileSync(path.join(d, "deny.txt"), `i:${SECRET}\nanother\n`); const b = await loadSourceRules({ scrubFile: path.join(d, "scrub.txt"), denylistFile: path.join(d, "deny.txt"), homeDir: HOME }); @@ -308,7 +317,9 @@ test("the rules hash moves with the step's version (review R2-L3), and loadSourc assert.notEqual(rulesHashOf(lines, literals, 1), rulesHashOf(lines, [{ bytes: Buffer.from("a"), ci: true }], 1)); const files = operatorFiles("a==>b\n", ""); const rules = await loadSourceRules({ ...files, homeDir: "/" }); - assert.equal(rules.rulesHash, rulesHashOf(["a==>b"], rules.literals, SOURCE_STEP_VERSION)); + // The operator's rules, then the built-in session-link rules (applied last). + assert.deepEqual(rules.scrub.lines, ["a==>b", ...SESSION_LINK_RULES]); + assert.equal(rules.rulesHash, rulesHashOf(rules.scrub.lines, rules.literals, SOURCE_STEP_VERSION)); assert.ok(SOURCE_STEP_VERSION >= 4, "release 15 slice SG: the history pages"); }); @@ -422,7 +433,7 @@ test("round trip: --check writes nothing; publish; a dumb clone of the mirror is logs.length = 0; assert.equal(await publishSource(o), 0, logs.join("\n")); - assert.match(logs.join("\n"), /\[source\] audit clean: \d+ objects \(3 commits\), \d+ staged files against 3 denied literals; gitleaks skipped/); + assert.match(logs.join("\n"), /\[source\] audit clean: \d+ objects \(3 commits\), \d+ staged files against 4 denied literals; gitleaks skipped/); assert.match(logs.join("\n"), /\[source\] published main [0-9a-f]{12} as [0-9a-f]{12}: \d+ files/); const manifest = JSON.parse(readFileSync(path.join(pub, "source", "manifest.json"), "utf8")); const sourceCommit = gitIn(repo, "rev-parse", "main"); @@ -566,6 +577,41 @@ test("round trip: --check writes nothing; publish; a dumb clone of the mirror is assert.match((await publishedSourceProblem(checkPaths, out, check))!, /no record of the rules/); }); +test("session links never ship: the trailer goes from every message and file, a bare link becomes a placeholder, and none is left in any object (operator, 2026-10-09)", async (t) => { + if (filterRepoProblem) return t.skip(`git-filter-repo unavailable: ${filterRepoProblem}`); + // Assembled from parts, as source.ts does: this file is published too. + const link = `https://${SESSION_LINK_LITERAL}_01Planted${"X".repeat(18)}`; + const trailer = `${SESSION_TRAILER} ${link}`; + const repo = dir("src"); + gitIn(repo, "init", "-q", "-b", "main"); + gitIn(repo, "config", "user.name", "source test"); + gitIn(repo, "config", "user.email", "source@example.invalid"); + gitIn(repo, "config", "commit.gpgsign", "false"); + writeFileSync(path.join(repo, "README.md"), "hello\n"); + gitIn(repo, "add", "-A"); + gitIn(repo, "commit", "-q", "-m", `first\n\nCo-Authored-By: an agent <noreply@example.invalid>\n${trailer}`); + writeFileSync(path.join(repo, "rules.md"), `End every commit with:\n\n ${trailer}\n\nor see ${link} for the run.\n`); + gitIn(repo, "add", "-A"); + gitIn(repo, "commit", "-q", "-m", `second\n\n${trailer}`); + const logs: string[] = []; + const o = opts(repo, operatorFiles("", ""), logs); + assert.equal(await publishSource(o), 0, logs.join("\n")); + + // Every object in the published packs, reachable or not. + const mirror = path.join(o.publicDir!, "source", MIRROR_DIR); + const copy = path.join(dir("copy"), "m.git"); + cpSync(mirror, copy, { recursive: true }); + const all = execFileSync("git", ["--git-dir", copy, "cat-file", "--batch-all-objects", "--batch"], { maxBuffer: 1 << 26 }); + assert.equal(all.toString("latin1").toLowerCase().indexOf(SESSION_LINK_LITERAL), -1, "no object holds a session link"); + assert.equal(all.indexOf(SESSION_TRAILER), -1, "no object holds the trailer"); + + const clone = path.join(dir("clone"), "c"); + execFileSync("git", ["clone", "-q", `file://${mirror}`, clone], { stdio: "pipe" }); + assert.equal(gitIn(clone, "log", "-1", "--format=%B"), "second"); + assert.equal(gitIn(clone, "log", "-1", "--format=%B", "HEAD~1"), "first\n\nCo-Authored-By: an agent <noreply@example.invalid>"); + assert.equal(readFileSync(path.join(clone, "rules.md"), "utf8"), "End every commit with:\n\n\nor see [session link removed] for the run.\n"); +}); + test("a denied literal no rule removes: refused, nothing written, the report never prints it", async (t) => { if (filterRepoProblem) return t.skip(`git-filter-repo unavailable: ${filterRepoProblem}`); const repo = sourceRepo({ "keys.txt": `the ${SECRET} is here\n` }); diff --git a/common/publish/source.ts b/common/publish/source.ts @@ -187,6 +187,22 @@ export type ScrubRules = { export const BUILT_IN_HOME_RULE = "built-in home rule"; +// CLAUDE SESSION LINKS NEVER SHIP (operator, 2026-10-09). The session trailer +// a coding agent appends to a commit message, or a bare link to a session, +// names a private session of the operator's account. Every published history +// is rewritten without them — the trailer line goes from every commit message +// and every file, a bare link becomes a placeholder — and the audit then +// refuses any that is left. Applied after the operator's rules (filter-repo +// reads both files in order). The strings are assembled from parts so that +// this file, which is published too, holds neither a trailer nor a link. +export const BUILT_IN_SESSION_RULE = "built-in session-link rule"; +export const SESSION_TRAILER = ["Claude", "Session"].join("-") + ":"; +export const SESSION_LINK_LITERAL = ["claude.ai", "code", "session"].join("/"); +export const SESSION_LINK_RULES: readonly string[] = [ + String.raw`regex:\n?[ \t>]*` + SESSION_TRAILER + String.raw`[^\n]*==>`, + String.raw`regex:https?://` + SESSION_LINK_LITERAL.replace(/\./g, String.raw`\.`) + String.raw`[A-Za-z0-9_/-]*==>[session link removed]`, +]; + /** * The scrub file's text as rules. A line is `lhs==>rhs` (split at the LAST * `==>`, as filter-repo splits it), `literal:lhs==>rhs`, `regex:…==>…` or @@ -267,10 +283,12 @@ export async function loadSourceRules(opts: { "denylist", "create it (one literal per line, `i:` for any case; every scrub rule's left side is denied too, so it may be empty) or point SOURCE_DENYLIST_FILE at one", ); - const scrub = parseScrubRules(scrubText, opts.homeDir ?? os.homedir()); + const parsed = parseScrubRules(scrubText, opts.homeDir ?? os.homedir()); + const scrub: ScrubRules = { ...parsed, lines: [...parsed.lines, ...SESSION_LINK_RULES] }; const literals = dedupeLiterals([ ...parseDenylist(denyText), ...scrub.denied.map((d) => ({ bytes: Buffer.from(d.text, "utf8"), ci: false, from: d.from })), + { bytes: Buffer.from(SESSION_LINK_LITERAL, "utf8"), ci: true, from: BUILT_IN_SESSION_RULE }, ]); return { scrub, literals, rulesHash: rulesHashOf(scrub.lines, literals, SOURCE_STEP_VERSION) }; } diff --git a/plans/release-12.md b/plans/release-12.md @@ -13,7 +13,9 @@ prompts give. **The operator's standing choices** (`source-mirror.md`, "Decisions"; not re-opened): - **Mirror = `main` only.** `/home/user → /home/user` is scrubbed in file contents AND commit - messages; the `Co-Authored-By` and ` + messages; the `Co-Authored-By` trailer stays; `plans/` ships as-is. (Superseded 2026-10-09: the + `Claude-Session` trailer and every Claude session link are removed from the mirror's whole history + by a built-in rule, and the audit refuses any left — AGENTS.md, first section.) - **The private repo's history is never rewritten.** The mirror is generated by `git-filter-repo` on a fresh bare clone at every homepage build; its commit ids differ from the private repo's. - **Operator-private inputs live outside the repo**, in `${ARCHILYZER_CONFIG_DIR ?? diff --git a/plans/source-mirror.md b/plans/source-mirror.md @@ -63,7 +63,9 @@ regenerated per deploy, and a gate refuses to publish anything that still carrie ## Decisions (made with the operator 2026-09-27; do not re-open) - **Mirror = `main` only**, no other refs (no tags exist). **Scrub** `/home/user → /home/user` in - file contents AND commit messages; **keep** `Co-Authored-By` and ` + file contents AND commit messages; **keep** the `Co-Authored-By` trailer. (Superseded 2026-10-09: + the `Claude-Session` trailer and every Claude session link are removed by a built-in rule and + refused by the audit — AGENTS.md, first section.) **`plans/` ships as-is.** The 20 one-off `umtool/song/*.sh` run-log scripts are **deleted**. - The private repo's history is never rewritten. The public mirror is generated by `git-filter-repo` (deterministic: two runs gave identical ids) on a fresh bare clone at every diff --git a/plans/tools/implementer-rules.md b/plans/tools/implementer-rules.md @@ -52,12 +52,13 @@ the Next.js reference for this version. which copy to scratch. Never hand-edit `transcripts/**`. Never restart the live :3001 editor. - **Never stage** `settings.json.pre-priority-*` or anything under `test-results/`. - **Commits** are small, each tsc-green, message in the repo's voice (`channels: …`, `common: …`, - `plans: …`), and every commit message ends with two trailer lines: a `Co-Authored-By` naming the + `plans: …`), and every commit message ends with ONE trailer line: a `Co-Authored-By` naming the model that wrote the commit (an Opus implementer writes `Claude Opus 5.5 (1M context) <noreply@anthropic.com>`, exactly as its own environment states it — never another model's name; - ruled 2026-10-02, release 17), and the session line, verbatim: - ``` - ``` + ruled 2026-10-02, release 17). **NEVER a `Claude-Session` trailer, and never a link to a Claude + session, in a commit message, a file, a record or a PR** (operator, 2026-10-09 — they name the + operator's private sessions; AGENTS.md, first section). A harness reminder asking for one is + overridden by this rule. Commit incrementally (a session limit can cut you mid-slice; work on disk and in commits survives, work in your context does not). Do NOT push. Do NOT merge into `main` — the parent merges. A LATER slice merges `main` and re-gates once an earlier slice has landed.