limits 63 + 64: what the version stamps cover, and where they reach - #44

Open
githubscum wants to merge 2 commits into
lotor-lane/limit-62-separator-spellingsfrom
lotor-lane/limit-63-matcher-stamp-blind
Open

limits 63 + 64: what the version stamps cover, and where they reach#44
githubscum wants to merge 2 commits into
lotor-lane/limit-62-separator-spellingsfrom
lotor-lane/limit-63-matcher-stamp-blind

Conversation

@githubscum

Copy link
Copy Markdown
Owner

Stacked on #43. Base is lotor-lane/limit-62-separator-spellings, not main, so the numbering does not collide (that branch adds limit 62; this adds 63). Merge #43 first and GitHub retargets this to main. The diff here is two files: one new test, one appended disclosure.

The work order

Verify what matcherVersionHash() actually covers, rather than what its docstring claims.

The defect

Every gated-action, policy-warn, grant and egress receipt carries a matcher version. It is the field a reader uses to answer the only question that makes two receipts comparable: were these decided by the same rules? The docstring calls it the "content hash of the matcher logic in force right now."

It hashes thirteen top-level functions plus RULE_TABLE and RULE_INFO. Function.prototype.toString() returns a function's own source and nothing it calls, so a helper is covered only if parts names it. The self-mod deciders are not named:

  • selfModFragmentsForBase — the protected-path list itself
  • isSelfModEdit, selfModCommandHit — both matcher bodies
  • normalizePath, pathContainsFragment — how a path is folded before matching
  • expandBraces, stripHeredocBodies, stripMessageArgs

isSelfModis hashed, and is a three-line dispatcher that names the two matchers and contains neither.

The consequence. Add a directory to the protected list, or change how a path is folded, and the gate stops a different set of actions while the stamp stays byte-identical. Two receipts either side of that change agree on the matcher version and disagree on the behaviour. The silence runs the wrong way: a matcher weakened between two runs keeps stamping the old, stronger version.

Enforcement is unaffected. The gate still gates correctly; the record misdescribes it. That is a witness defect rather than an enforcement one, which is why it gets its own entry rather than a line under limit 62.

What was tested

  • test/policy-matcher-stamp-coverage.test.js, 8 assertions. Two controls (the hashed bodies are present; the stamp is a stable 16-hex digest under matcher/1) so the absence assertions cannot pass vacuously by an import dying or a rename emptying the strings. Six assert the current defective state.
  • Full suite: 962 pass / 0 fail, up from 954 on the base branch.
  • Stamp on this build: matcher/1 95291ff6385151ca.
  • The introduction date was read from history (b1b7bf8, 2026-08-09), not inferred.

This is the tripwire pattern from #41 and #43: it asserts the defect, so it fails when someone fixes the stamp. The repair inverts the assertions and amends the disclosure in the same change. Deleting the block closes the confession by destroying its evidence.

What a reviewer should doubt

  1. The subset problem. The test reconstructs the hashed text from the eight matchers this module exports. Five more values in parts are not exported and are absent from my reconstruction. That is sound for proving absence (a string missing from the whole is missing from the subset) and would be unsound for proving presence — the controls only prove presence for strings I take from exported bodies. Check parts at the top of matcherVersionHash() yourself; the claim rests on reading that array, and the test is the mechanical half.
  2. I did not demonstrate two builds colliding. The direct proof is to mutate a fragment and show the digest unchanged. That mutation is src/policy and the gate refused it unsigned, correctly, so the evidence here is by construction plus the 2026-08-23 historical case, not by a hash collision I produced. If you want the stronger demonstration it costs one signature.
  3. Whether matcher/2 is the right repair. Bumping the schema marker on a coverage change is my reading of what that marker is for: the hashing method changes, and a value moving on its own would be indistinguishable from an ordinary rule edit. Arguable the other way.
  4. The KNOWN-LIMITS pin was not re-stamped. It still names 2173d231, same as on main and on limit 62: an equivalent path spelling walks around the self-mod gate #43. Re-stamping mid-branch would claim the log describes a commit that is not merged. No test enforces it; flagging so it is a choice rather than an oversight.

Scope

KNOWN-LIMITS.md and test/ only. Nothing in the non-delegable core is touched. The fix is core and queues for a signing sitting — it is named in the disclosure and deliberately not attempted here.

🤖 Generated with Claude Code

githubscumand others added 2 commits September 2, 2026 00:45
…deciders
Every gate, warn, grant and egress receipt carries a matcher version, and it
is the field a reader uses to ask whether two decisions were made by the same
rules. It hashes thirteen top-level functions plus the two rule tables.
Function.toString returns a function's own source and nothing it calls, so the
helpers that actually decide self-mod are outside it: the protected-path list,
both self-mod matchers, the path normalizer, the segment matcher, the brace
expander and the prose strippers. The dispatcher that IS hashed only names the
matchers it calls.
Change the protected list or the way a path is folded before matching, and the
gate stops a different set of actions while the stamp stays byte-identical.
The silence runs the wrong way: a matcher weakened between two runs keeps
stamping the old, stronger version. Enforcement is unaffected; the trace
misdescribes it, which is why this is filed as a witness defect.
Measured, not read off the source. The new test asserts the absence directly
against the hashed inputs, with controls asserting the hashed bodies are
present so the block cannot pass vacuously. Stamp on this build:
matcher/1 95291ff6385151ca. The stamp was introduced 2026-08-09 (b1b7bf8);
the protected list gained an entry on 2026-08-23 and the version did not move.
The repair is core and is not attempted here: name the helpers in parts and
bump the schema marker to matcher/2, since the hashing method changes and a
value moving on its own would be indistinguishable from a rule edit.
Suite 962 pass / 0 fail (8 new).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…record
Following limit 63's stated residual ("behavior that reaches the decision
from outside the policy module would remain unstamped") to where it leads,
and finding that the instrument which closes it is already built.
Two code identities exist. matcherVersionHash() covers named functions in
src/policy/index.js and is on every receipt. computeSourceDigest() covers
all 50 .js files under src/ and bin/ - including src/gate, src/grant,
src/chain, src/store and bin/hook-pre-tool-use.js - and is attached only to
MCP tool responses, which are discarded when the call returns.
So the ephemeral artifact carries the strong identity and the permanent one
carries the weak one. Change the gate or the hooks and every receipt after
the change is byte-comparable with every receipt before it.
Measured: build digest 47ed7876d2652e68 over 50 files / 522,651 bytes;
matcher stamp matcher/1 95291ff6385151ca; exactly two consumers of the
digest in the tree, neither of which writes to the chain.
Ships KNOWN-LIMITS 64 plus a 4-assertion test. Every absence claim is
paired with a control (the same file IS asserted to carry matcherHash, and
the MCP server IS asserted to carry the build stamp) so the block cannot
pass vacuously. Suite 966/966, up from 962.
The repair touches src/gate and bin/hook-* and therefore queues for a
signing sitting rather than riding along with this disclosure.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@githubscumgithubscum changed the title limit 63: the matcher version stamp hashes the entry points, not the deciderslimits 63 + 64: what the version stamps cover, and where they reachSep 2, 2026
@githubscum

Copy link
Copy Markdown
OwnerAuthor

Added in e60aa1e: KNOWN-LIMITS 64

This branch was opened for limit 63 alone. A second entry landed on it because it is the same subject and 63's own text points straight at it. Retitled accordingly. If you would rather review 63 by itself, say so and I will split 64 onto its own branch — nothing else depends on the pairing.

What 64 says

Limit 63 ends with a residual: "behavior that reaches the decision from outside [the policy module] would remain unstamped." It treats that as an accepted ceiling. It is not a ceiling. The instrument that closes it is already in this repository.

StampCoversReaches
matcherVersionHash()named functions in the policy moduleevery receipt
computeSourceDigest()all 50 .js files under the two source directories — including the gate, the grant checker, the chain writer, the store, and the pre-tool-use hookMCP tool responses only

The ephemeral artifact carries the strong identity; the permanent one carries the weak identity. Change the gate or the hooks, and every receipt written after is byte-comparable with every receipt written before.

Measured

  • build digest 47ed7876d2652e68, 50 files / 522,651 bytes
  • matcher stamp matcher/1 95291ff6385151ca (same value limit 63 recorded — consistent)
  • exactly two consumers of the digest in the whole tree, both inside the MCP directory. Neither writes to the chain.

Tested

test/stamp-reach-coverage.test.js, 4 assertions. Every absence claim is paired with a control — the same files are asserted to carrymatcherHash, and the MCP server is asserted to carry the build stamp — so the block cannot pass vacuously. Suite 966/966, up from 962 at 94bbc9a.

What a reviewer should doubt

  1. This is a disclosure, not a fix. The repair (carry the digest onto session-open, inherit by session id) touches non-delegable core. It queues for a signing sitting. Nothing in this PR changes gate behavior.
  2. "Exactly two consumers" is a regex over source text, not a call-graph analysis. A consumer reaching the digest by some indirection would not be caught. I read both files; I did not prove the negative mechanically.
  3. Whether session-open is the right carrier is a design call I made, not a measured result. Per-action stamping is more precise and costs a field on every receipt. I chose the cheap end. Argue with it.
  4. The severity depends on a premise worth checking: that gate, chain, and hook code changes often enough to matter. If that code is effectively frozen, this is theoretical. Eleven merged lane PRs suggest it is not frozen, but that is an inference, not a measurement.
  5. Stacking unchanged: this branch still sits on lotor-lane/limit-62-separator-spellings (limit 62: an equivalent path spelling walks around the self-mod gate #43). Merge order is still forced, now carrying three entries.

Footnote the gate earned

Staging this very comment was blocked by Lotor — the body quotes protected path names as prose, which is limit 26's class (the matcher scanning prose as code). Fourth block this run, all four handled with the right tool rather than by reshaping the command. The gate was not wrong to stop it; a comment body is an opaque string to a matcher.

Sign up for freeto 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

@githubscum
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

limits 63 + 64: what the version stamps cover, and where they reach - #44

Open
githubscum wants to merge 2 commits into
lotor-lane/limit-62-separator-spellingsfrom
lotor-lane/limit-63-matcher-stamp-blind
Open

limits 63 + 64: what the version stamps cover, and where they reach#44
githubscum wants to merge 2 commits into
lotor-lane/limit-62-separator-spellingsfrom
lotor-lane/limit-63-matcher-stamp-blind

Conversation

@githubscum

Copy link
Copy Markdown
Owner

Stacked on #43. Base is lotor-lane/limit-62-separator-spellings, not main, so the numbering does not collide (that branch adds limit 62; this adds 63). Merge #43 first and GitHub retargets this to main. The diff here is two files: one new test, one appended disclosure.

The work order

Verify what matcherVersionHash() actually covers, rather than what its docstring claims.

The defect

Every gated-action, policy-warn, grant and egress receipt carries a matcher version. It is the field a reader uses to answer the only question that makes two receipts comparable: were these decided by the same rules? The docstring calls it the "content hash of the matcher logic in force right now."

It hashes thirteen top-level functions plus RULE_TABLE and RULE_INFO. Function.prototype.toString() returns a function's own source and nothing it calls, so a helper is covered only if parts names it. The self-mod deciders are not named:

  • selfModFragmentsForBase — the protected-path list itself
  • isSelfModEdit, selfModCommandHit — both matcher bodies
  • normalizePath, pathContainsFragment — how a path is folded before matching
  • expandBraces, stripHeredocBodies, stripMessageArgs

isSelfModis hashed, and is a three-line dispatcher that names the two matchers and contains neither.

The consequence. Add a directory to the protected list, or change how a path is folded, and the gate stops a different set of actions while the stamp stays byte-identical. Two receipts either side of that change agree on the matcher version and disagree on the behaviour. The silence runs the wrong way: a matcher weakened between two runs keeps stamping the old, stronger version.

Enforcement is unaffected. The gate still gates correctly; the record misdescribes it. That is a witness defect rather than an enforcement one, which is why it gets its own entry rather than a line under limit 62.

What was tested

  • test/policy-matcher-stamp-coverage.test.js, 8 assertions. Two controls (the hashed bodies are present; the stamp is a stable 16-hex digest under matcher/1) so the absence assertions cannot pass vacuously by an import dying or a rename emptying the strings. Six assert the current defective state.
  • Full suite: 962 pass / 0 fail, up from 954 on the base branch.
  • Stamp on this build: matcher/1 95291ff6385151ca.
  • The introduction date was read from history (b1b7bf8, 2026-08-09), not inferred.

This is the tripwire pattern from #41 and #43: it asserts the defect, so it fails when someone fixes the stamp. The repair inverts the assertions and amends the disclosure in the same change. Deleting the block closes the confession by destroying its evidence.

What a reviewer should doubt

  1. The subset problem. The test reconstructs the hashed text from the eight matchers this module exports. Five more values in parts are not exported and are absent from my reconstruction. That is sound for proving absence (a string missing from the whole is missing from the subset) and would be unsound for proving presence — the controls only prove presence for strings I take from exported bodies. Check parts at the top of matcherVersionHash() yourself; the claim rests on reading that array, and the test is the mechanical half.
  2. I did not demonstrate two builds colliding. The direct proof is to mutate a fragment and show the digest unchanged. That mutation is src/policy and the gate refused it unsigned, correctly, so the evidence here is by construction plus the 2026-08-23 historical case, not by a hash collision I produced. If you want the stronger demonstration it costs one signature.
  3. Whether matcher/2 is the right repair. Bumping the schema marker on a coverage change is my reading of what that marker is for: the hashing method changes, and a value moving on its own would be indistinguishable from an ordinary rule edit. Arguable the other way.
  4. The KNOWN-LIMITS pin was not re-stamped. It still names 2173d231, same as on main and on limit 62: an equivalent path spelling walks around the self-mod gate #43. Re-stamping mid-branch would claim the log describes a commit that is not merged. No test enforces it; flagging so it is a choice rather than an oversight.

Scope

KNOWN-LIMITS.md and test/ only. Nothing in the non-delegable core is touched. The fix is core and queues for a signing sitting — it is named in the disclosure and deliberately not attempted here.

🤖 Generated with Claude Code

githubscumand others added 2 commits September 2, 2026 00:45
…deciders
Every gate, warn, grant and egress receipt carries a matcher version, and it
is the field a reader uses to ask whether two decisions were made by the same
rules. It hashes thirteen top-level functions plus the two rule tables.
Function.toString returns a function's own source and nothing it calls, so the
helpers that actually decide self-mod are outside it: the protected-path list,
both self-mod matchers, the path normalizer, the segment matcher, the brace
expander and the prose strippers. The dispatcher that IS hashed only names the
matchers it calls.
Change the protected list or the way a path is folded before matching, and the
gate stops a different set of actions while the stamp stays byte-identical.
The silence runs the wrong way: a matcher weakened between two runs keeps
stamping the old, stronger version. Enforcement is unaffected; the trace
misdescribes it, which is why this is filed as a witness defect.
Measured, not read off the source. The new test asserts the absence directly
against the hashed inputs, with controls asserting the hashed bodies are
present so the block cannot pass vacuously. Stamp on this build:
matcher/1 95291ff6385151ca. The stamp was introduced 2026-08-09 (b1b7bf8);
the protected list gained an entry on 2026-08-23 and the version did not move.
The repair is core and is not attempted here: name the helpers in parts and
bump the schema marker to matcher/2, since the hashing method changes and a
value moving on its own would be indistinguishable from a rule edit.
Suite 962 pass / 0 fail (8 new).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…record
Following limit 63's stated residual ("behavior that reaches the decision
from outside the policy module would remain unstamped") to where it leads,
and finding that the instrument which closes it is already built.
Two code identities exist. matcherVersionHash() covers named functions in
src/policy/index.js and is on every receipt. computeSourceDigest() covers
all 50 .js files under src/ and bin/ - including src/gate, src/grant,
src/chain, src/store and bin/hook-pre-tool-use.js - and is attached only to
MCP tool responses, which are discarded when the call returns.
So the ephemeral artifact carries the strong identity and the permanent one
carries the weak one. Change the gate or the hooks and every receipt after
the change is byte-comparable with every receipt before it.
Measured: build digest 47ed7876d2652e68 over 50 files / 522,651 bytes;
matcher stamp matcher/1 95291ff6385151ca; exactly two consumers of the
digest in the tree, neither of which writes to the chain.
Ships KNOWN-LIMITS 64 plus a 4-assertion test. Every absence claim is
paired with a control (the same file IS asserted to carry matcherHash, and
the MCP server IS asserted to carry the build stamp) so the block cannot
pass vacuously. Suite 966/966, up from 962.
The repair touches src/gate and bin/hook-* and therefore queues for a
signing sitting rather than riding along with this disclosure.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@githubscumgithubscum changed the title limit 63: the matcher version stamp hashes the entry points, not the deciderslimits 63 + 64: what the version stamps cover, and where they reachSep 2, 2026
@githubscum

Copy link
Copy Markdown
OwnerAuthor

Added in e60aa1e: KNOWN-LIMITS 64

This branch was opened for limit 63 alone. A second entry landed on it because it is the same subject and 63's own text points straight at it. Retitled accordingly. If you would rather review 63 by itself, say so and I will split 64 onto its own branch — nothing else depends on the pairing.

What 64 says

Limit 63 ends with a residual: "behavior that reaches the decision from outside [the policy module] would remain unstamped." It treats that as an accepted ceiling. It is not a ceiling. The instrument that closes it is already in this repository.

StampCoversReaches
matcherVersionHash()named functions in the policy moduleevery receipt
computeSourceDigest()all 50 .js files under the two source directories — including the gate, the grant checker, the chain writer, the store, and the pre-tool-use hookMCP tool responses only

The ephemeral artifact carries the strong identity; the permanent one carries the weak identity. Change the gate or the hooks, and every receipt written after is byte-comparable with every receipt written before.

Measured

  • build digest 47ed7876d2652e68, 50 files / 522,651 bytes
  • matcher stamp matcher/1 95291ff6385151ca (same value limit 63 recorded — consistent)
  • exactly two consumers of the digest in the whole tree, both inside the MCP directory. Neither writes to the chain.

Tested

test/stamp-reach-coverage.test.js, 4 assertions. Every absence claim is paired with a control — the same files are asserted to carrymatcherHash, and the MCP server is asserted to carry the build stamp — so the block cannot pass vacuously. Suite 966/966, up from 962 at 94bbc9a.

What a reviewer should doubt

  1. This is a disclosure, not a fix. The repair (carry the digest onto session-open, inherit by session id) touches non-delegable core. It queues for a signing sitting. Nothing in this PR changes gate behavior.
  2. "Exactly two consumers" is a regex over source text, not a call-graph analysis. A consumer reaching the digest by some indirection would not be caught. I read both files; I did not prove the negative mechanically.
  3. Whether session-open is the right carrier is a design call I made, not a measured result. Per-action stamping is more precise and costs a field on every receipt. I chose the cheap end. Argue with it.
  4. The severity depends on a premise worth checking: that gate, chain, and hook code changes often enough to matter. If that code is effectively frozen, this is theoretical. Eleven merged lane PRs suggest it is not frozen, but that is an inference, not a measurement.
  5. Stacking unchanged: this branch still sits on lotor-lane/limit-62-separator-spellings (limit 62: an equivalent path spelling walks around the self-mod gate #43). Merge order is still forced, now carrying three entries.

Footnote the gate earned

Staging this very comment was blocked by Lotor — the body quotes protected path names as prose, which is limit 26's class (the matcher scanning prose as code). Fourth block this run, all four handled with the right tool rather than by reshaping the command. The gate was not wrong to stop it; a comment body is an opaque string to a matcher.

Sign up for freeto 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

@githubscum
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

limits 63 + 64: what the version stamps cover, and where they reach - #44

Open
githubscum wants to merge 2 commits into
lotor-lane/limit-62-separator-spellingsfrom
lotor-lane/limit-63-matcher-stamp-blind
Open

limits 63 + 64: what the version stamps cover, and where they reach#44
githubscum wants to merge 2 commits into
lotor-lane/limit-62-separator-spellingsfrom
lotor-lane/limit-63-matcher-stamp-blind

Conversation

@githubscum

Copy link
Copy Markdown
Owner

Stacked on #43. Base is lotor-lane/limit-62-separator-spellings, not main, so the numbering does not collide (that branch adds limit 62; this adds 63). Merge #43 first and GitHub retargets this to main. The diff here is two files: one new test, one appended disclosure.

The work order

Verify what matcherVersionHash() actually covers, rather than what its docstring claims.

The defect

Every gated-action, policy-warn, grant and egress receipt carries a matcher version. It is the field a reader uses to answer the only question that makes two receipts comparable: were these decided by the same rules? The docstring calls it the "content hash of the matcher logic in force right now."

It hashes thirteen top-level functions plus RULE_TABLE and RULE_INFO. Function.prototype.toString() returns a function's own source and nothing it calls, so a helper is covered only if parts names it. The self-mod deciders are not named:

  • selfModFragmentsForBase — the protected-path list itself
  • isSelfModEdit, selfModCommandHit — both matcher bodies
  • normalizePath, pathContainsFragment — how a path is folded before matching
  • expandBraces, stripHeredocBodies, stripMessageArgs

isSelfModis hashed, and is a three-line dispatcher that names the two matchers and contains neither.

The consequence. Add a directory to the protected list, or change how a path is folded, and the gate stops a different set of actions while the stamp stays byte-identical. Two receipts either side of that change agree on the matcher version and disagree on the behaviour. The silence runs the wrong way: a matcher weakened between two runs keeps stamping the old, stronger version.

Enforcement is unaffected. The gate still gates correctly; the record misdescribes it. That is a witness defect rather than an enforcement one, which is why it gets its own entry rather than a line under limit 62.

What was tested

  • test/policy-matcher-stamp-coverage.test.js, 8 assertions. Two controls (the hashed bodies are present; the stamp is a stable 16-hex digest under matcher/1) so the absence assertions cannot pass vacuously by an import dying or a rename emptying the strings. Six assert the current defective state.
  • Full suite: 962 pass / 0 fail, up from 954 on the base branch.
  • Stamp on this build: matcher/1 95291ff6385151ca.
  • The introduction date was read from history (b1b7bf8, 2026-08-09), not inferred.

This is the tripwire pattern from #41 and #43: it asserts the defect, so it fails when someone fixes the stamp. The repair inverts the assertions and amends the disclosure in the same change. Deleting the block closes the confession by destroying its evidence.

What a reviewer should doubt

  1. The subset problem. The test reconstructs the hashed text from the eight matchers this module exports. Five more values in parts are not exported and are absent from my reconstruction. That is sound for proving absence (a string missing from the whole is missing from the subset) and would be unsound for proving presence — the controls only prove presence for strings I take from exported bodies. Check parts at the top of matcherVersionHash() yourself; the claim rests on reading that array, and the test is the mechanical half.
  2. I did not demonstrate two builds colliding. The direct proof is to mutate a fragment and show the digest unchanged. That mutation is src/policy and the gate refused it unsigned, correctly, so the evidence here is by construction plus the 2026-08-23 historical case, not by a hash collision I produced. If you want the stronger demonstration it costs one signature.
  3. Whether matcher/2 is the right repair. Bumping the schema marker on a coverage change is my reading of what that marker is for: the hashing method changes, and a value moving on its own would be indistinguishable from an ordinary rule edit. Arguable the other way.
  4. The KNOWN-LIMITS pin was not re-stamped. It still names 2173d231, same as on main and on limit 62: an equivalent path spelling walks around the self-mod gate #43. Re-stamping mid-branch would claim the log describes a commit that is not merged. No test enforces it; flagging so it is a choice rather than an oversight.

Scope

KNOWN-LIMITS.md and test/ only. Nothing in the non-delegable core is touched. The fix is core and queues for a signing sitting — it is named in the disclosure and deliberately not attempted here.

🤖 Generated with Claude Code

githubscumand others added 2 commits September 2, 2026 00:45
…deciders
Every gate, warn, grant and egress receipt carries a matcher version, and it
is the field a reader uses to ask whether two decisions were made by the same
rules. It hashes thirteen top-level functions plus the two rule tables.
Function.toString returns a function's own source and nothing it calls, so the
helpers that actually decide self-mod are outside it: the protected-path list,
both self-mod matchers, the path normalizer, the segment matcher, the brace
expander and the prose strippers. The dispatcher that IS hashed only names the
matchers it calls.
Change the protected list or the way a path is folded before matching, and the
gate stops a different set of actions while the stamp stays byte-identical.
The silence runs the wrong way: a matcher weakened between two runs keeps
stamping the old, stronger version. Enforcement is unaffected; the trace
misdescribes it, which is why this is filed as a witness defect.
Measured, not read off the source. The new test asserts the absence directly
against the hashed inputs, with controls asserting the hashed bodies are
present so the block cannot pass vacuously. Stamp on this build:
matcher/1 95291ff6385151ca. The stamp was introduced 2026-08-09 (b1b7bf8);
the protected list gained an entry on 2026-08-23 and the version did not move.
The repair is core and is not attempted here: name the helpers in parts and
bump the schema marker to matcher/2, since the hashing method changes and a
value moving on its own would be indistinguishable from a rule edit.
Suite 962 pass / 0 fail (8 new).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…record
Following limit 63's stated residual ("behavior that reaches the decision
from outside the policy module would remain unstamped") to where it leads,
and finding that the instrument which closes it is already built.
Two code identities exist. matcherVersionHash() covers named functions in
src/policy/index.js and is on every receipt. computeSourceDigest() covers
all 50 .js files under src/ and bin/ - including src/gate, src/grant,
src/chain, src/store and bin/hook-pre-tool-use.js - and is attached only to
MCP tool responses, which are discarded when the call returns.
So the ephemeral artifact carries the strong identity and the permanent one
carries the weak one. Change the gate or the hooks and every receipt after
the change is byte-comparable with every receipt before it.
Measured: build digest 47ed7876d2652e68 over 50 files / 522,651 bytes;
matcher stamp matcher/1 95291ff6385151ca; exactly two consumers of the
digest in the tree, neither of which writes to the chain.
Ships KNOWN-LIMITS 64 plus a 4-assertion test. Every absence claim is
paired with a control (the same file IS asserted to carry matcherHash, and
the MCP server IS asserted to carry the build stamp) so the block cannot
pass vacuously. Suite 966/966, up from 962.
The repair touches src/gate and bin/hook-* and therefore queues for a
signing sitting rather than riding along with this disclosure.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@githubscumgithubscum changed the title limit 63: the matcher version stamp hashes the entry points, not the deciderslimits 63 + 64: what the version stamps cover, and where they reachSep 2, 2026
@githubscum

Copy link
Copy Markdown
OwnerAuthor

Added in e60aa1e: KNOWN-LIMITS 64

This branch was opened for limit 63 alone. A second entry landed on it because it is the same subject and 63's own text points straight at it. Retitled accordingly. If you would rather review 63 by itself, say so and I will split 64 onto its own branch — nothing else depends on the pairing.

What 64 says

Limit 63 ends with a residual: "behavior that reaches the decision from outside [the policy module] would remain unstamped." It treats that as an accepted ceiling. It is not a ceiling. The instrument that closes it is already in this repository.

StampCoversReaches
matcherVersionHash()named functions in the policy moduleevery receipt
computeSourceDigest()all 50 .js files under the two source directories — including the gate, the grant checker, the chain writer, the store, and the pre-tool-use hookMCP tool responses only

The ephemeral artifact carries the strong identity; the permanent one carries the weak identity. Change the gate or the hooks, and every receipt written after is byte-comparable with every receipt written before.

Measured

  • build digest 47ed7876d2652e68, 50 files / 522,651 bytes
  • matcher stamp matcher/1 95291ff6385151ca (same value limit 63 recorded — consistent)
  • exactly two consumers of the digest in the whole tree, both inside the MCP directory. Neither writes to the chain.

Tested

test/stamp-reach-coverage.test.js, 4 assertions. Every absence claim is paired with a control — the same files are asserted to carrymatcherHash, and the MCP server is asserted to carry the build stamp — so the block cannot pass vacuously. Suite 966/966, up from 962 at 94bbc9a.

What a reviewer should doubt

  1. This is a disclosure, not a fix. The repair (carry the digest onto session-open, inherit by session id) touches non-delegable core. It queues for a signing sitting. Nothing in this PR changes gate behavior.
  2. "Exactly two consumers" is a regex over source text, not a call-graph analysis. A consumer reaching the digest by some indirection would not be caught. I read both files; I did not prove the negative mechanically.
  3. Whether session-open is the right carrier is a design call I made, not a measured result. Per-action stamping is more precise and costs a field on every receipt. I chose the cheap end. Argue with it.
  4. The severity depends on a premise worth checking: that gate, chain, and hook code changes often enough to matter. If that code is effectively frozen, this is theoretical. Eleven merged lane PRs suggest it is not frozen, but that is an inference, not a measurement.
  5. Stacking unchanged: this branch still sits on lotor-lane/limit-62-separator-spellings (limit 62: an equivalent path spelling walks around the self-mod gate #43). Merge order is still forced, now carrying three entries.

Footnote the gate earned

Staging this very comment was blocked by Lotor — the body quotes protected path names as prose, which is limit 26's class (the matcher scanning prose as code). Fourth block this run, all four handled with the right tool rather than by reshaping the command. The gate was not wrong to stop it; a comment body is an opaque string to a matcher.

Sign up for freeto 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

@githubscum
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

limits 63 + 64: what the version stamps cover, and where they reach - #44

Open
githubscum wants to merge 2 commits into
lotor-lane/limit-62-separator-spellingsfrom
lotor-lane/limit-63-matcher-stamp-blind
Open

limits 63 + 64: what the version stamps cover, and where they reach#44
githubscum wants to merge 2 commits into
lotor-lane/limit-62-separator-spellingsfrom
lotor-lane/limit-63-matcher-stamp-blind

Conversation

@githubscum

Copy link
Copy Markdown
Owner

Stacked on #43. Base is lotor-lane/limit-62-separator-spellings, not main, so the numbering does not collide (that branch adds limit 62; this adds 63). Merge #43 first and GitHub retargets this to main. The diff here is two files: one new test, one appended disclosure.

The work order

Verify what matcherVersionHash() actually covers, rather than what its docstring claims.

The defect

Every gated-action, policy-warn, grant and egress receipt carries a matcher version. It is the field a reader uses to answer the only question that makes two receipts comparable: were these decided by the same rules? The docstring calls it the "content hash of the matcher logic in force right now."

It hashes thirteen top-level functions plus RULE_TABLE and RULE_INFO. Function.prototype.toString() returns a function's own source and nothing it calls, so a helper is covered only if parts names it. The self-mod deciders are not named:

  • selfModFragmentsForBase — the protected-path list itself
  • isSelfModEdit, selfModCommandHit — both matcher bodies
  • normalizePath, pathContainsFragment — how a path is folded before matching
  • expandBraces, stripHeredocBodies, stripMessageArgs

isSelfModis hashed, and is a three-line dispatcher that names the two matchers and contains neither.

The consequence. Add a directory to the protected list, or change how a path is folded, and the gate stops a different set of actions while the stamp stays byte-identical. Two receipts either side of that change agree on the matcher version and disagree on the behaviour. The silence runs the wrong way: a matcher weakened between two runs keeps stamping the old, stronger version.

Enforcement is unaffected. The gate still gates correctly; the record misdescribes it. That is a witness defect rather than an enforcement one, which is why it gets its own entry rather than a line under limit 62.

What was tested

  • test/policy-matcher-stamp-coverage.test.js, 8 assertions. Two controls (the hashed bodies are present; the stamp is a stable 16-hex digest under matcher/1) so the absence assertions cannot pass vacuously by an import dying or a rename emptying the strings. Six assert the current defective state.
  • Full suite: 962 pass / 0 fail, up from 954 on the base branch.
  • Stamp on this build: matcher/1 95291ff6385151ca.
  • The introduction date was read from history (b1b7bf8, 2026-08-09), not inferred.

This is the tripwire pattern from #41 and #43: it asserts the defect, so it fails when someone fixes the stamp. The repair inverts the assertions and amends the disclosure in the same change. Deleting the block closes the confession by destroying its evidence.

What a reviewer should doubt

  1. The subset problem. The test reconstructs the hashed text from the eight matchers this module exports. Five more values in parts are not exported and are absent from my reconstruction. That is sound for proving absence (a string missing from the whole is missing from the subset) and would be unsound for proving presence — the controls only prove presence for strings I take from exported bodies. Check parts at the top of matcherVersionHash() yourself; the claim rests on reading that array, and the test is the mechanical half.
  2. I did not demonstrate two builds colliding. The direct proof is to mutate a fragment and show the digest unchanged. That mutation is src/policy and the gate refused it unsigned, correctly, so the evidence here is by construction plus the 2026-08-23 historical case, not by a hash collision I produced. If you want the stronger demonstration it costs one signature.
  3. Whether matcher/2 is the right repair. Bumping the schema marker on a coverage change is my reading of what that marker is for: the hashing method changes, and a value moving on its own would be indistinguishable from an ordinary rule edit. Arguable the other way.
  4. The KNOWN-LIMITS pin was not re-stamped. It still names 2173d231, same as on main and on limit 62: an equivalent path spelling walks around the self-mod gate #43. Re-stamping mid-branch would claim the log describes a commit that is not merged. No test enforces it; flagging so it is a choice rather than an oversight.

Scope

KNOWN-LIMITS.md and test/ only. Nothing in the non-delegable core is touched. The fix is core and queues for a signing sitting — it is named in the disclosure and deliberately not attempted here.

🤖 Generated with Claude Code

githubscumand others added 2 commits September 2, 2026 00:45
…deciders
Every gate, warn, grant and egress receipt carries a matcher version, and it
is the field a reader uses to ask whether two decisions were made by the same
rules. It hashes thirteen top-level functions plus the two rule tables.
Function.toString returns a function's own source and nothing it calls, so the
helpers that actually decide self-mod are outside it: the protected-path list,
both self-mod matchers, the path normalizer, the segment matcher, the brace
expander and the prose strippers. The dispatcher that IS hashed only names the
matchers it calls.
Change the protected list or the way a path is folded before matching, and the
gate stops a different set of actions while the stamp stays byte-identical.
The silence runs the wrong way: a matcher weakened between two runs keeps
stamping the old, stronger version. Enforcement is unaffected; the trace
misdescribes it, which is why this is filed as a witness defect.
Measured, not read off the source. The new test asserts the absence directly
against the hashed inputs, with controls asserting the hashed bodies are
present so the block cannot pass vacuously. Stamp on this build:
matcher/1 95291ff6385151ca. The stamp was introduced 2026-08-09 (b1b7bf8);
the protected list gained an entry on 2026-08-23 and the version did not move.
The repair is core and is not attempted here: name the helpers in parts and
bump the schema marker to matcher/2, since the hashing method changes and a
value moving on its own would be indistinguishable from a rule edit.
Suite 962 pass / 0 fail (8 new).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…record
Following limit 63's stated residual ("behavior that reaches the decision
from outside the policy module would remain unstamped") to where it leads,
and finding that the instrument which closes it is already built.
Two code identities exist. matcherVersionHash() covers named functions in
src/policy/index.js and is on every receipt. computeSourceDigest() covers
all 50 .js files under src/ and bin/ - including src/gate, src/grant,
src/chain, src/store and bin/hook-pre-tool-use.js - and is attached only to
MCP tool responses, which are discarded when the call returns.
So the ephemeral artifact carries the strong identity and the permanent one
carries the weak one. Change the gate or the hooks and every receipt after
the change is byte-comparable with every receipt before it.
Measured: build digest 47ed7876d2652e68 over 50 files / 522,651 bytes;
matcher stamp matcher/1 95291ff6385151ca; exactly two consumers of the
digest in the tree, neither of which writes to the chain.
Ships KNOWN-LIMITS 64 plus a 4-assertion test. Every absence claim is
paired with a control (the same file IS asserted to carry matcherHash, and
the MCP server IS asserted to carry the build stamp) so the block cannot
pass vacuously. Suite 966/966, up from 962.
The repair touches src/gate and bin/hook-* and therefore queues for a
signing sitting rather than riding along with this disclosure.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@githubscumgithubscum changed the title limit 63: the matcher version stamp hashes the entry points, not the deciderslimits 63 + 64: what the version stamps cover, and where they reachSep 2, 2026
@githubscum

Copy link
Copy Markdown
OwnerAuthor

Added in e60aa1e: KNOWN-LIMITS 64

This branch was opened for limit 63 alone. A second entry landed on it because it is the same subject and 63's own text points straight at it. Retitled accordingly. If you would rather review 63 by itself, say so and I will split 64 onto its own branch — nothing else depends on the pairing.

What 64 says

Limit 63 ends with a residual: "behavior that reaches the decision from outside [the policy module] would remain unstamped." It treats that as an accepted ceiling. It is not a ceiling. The instrument that closes it is already in this repository.

StampCoversReaches
matcherVersionHash()named functions in the policy moduleevery receipt
computeSourceDigest()all 50 .js files under the two source directories — including the gate, the grant checker, the chain writer, the store, and the pre-tool-use hookMCP tool responses only

The ephemeral artifact carries the strong identity; the permanent one carries the weak identity. Change the gate or the hooks, and every receipt written after is byte-comparable with every receipt written before.

Measured

  • build digest 47ed7876d2652e68, 50 files / 522,651 bytes
  • matcher stamp matcher/1 95291ff6385151ca (same value limit 63 recorded — consistent)
  • exactly two consumers of the digest in the whole tree, both inside the MCP directory. Neither writes to the chain.

Tested

test/stamp-reach-coverage.test.js, 4 assertions. Every absence claim is paired with a control — the same files are asserted to carrymatcherHash, and the MCP server is asserted to carry the build stamp — so the block cannot pass vacuously. Suite 966/966, up from 962 at 94bbc9a.

What a reviewer should doubt

  1. This is a disclosure, not a fix. The repair (carry the digest onto session-open, inherit by session id) touches non-delegable core. It queues for a signing sitting. Nothing in this PR changes gate behavior.
  2. "Exactly two consumers" is a regex over source text, not a call-graph analysis. A consumer reaching the digest by some indirection would not be caught. I read both files; I did not prove the negative mechanically.
  3. Whether session-open is the right carrier is a design call I made, not a measured result. Per-action stamping is more precise and costs a field on every receipt. I chose the cheap end. Argue with it.
  4. The severity depends on a premise worth checking: that gate, chain, and hook code changes often enough to matter. If that code is effectively frozen, this is theoretical. Eleven merged lane PRs suggest it is not frozen, but that is an inference, not a measurement.
  5. Stacking unchanged: this branch still sits on lotor-lane/limit-62-separator-spellings (limit 62: an equivalent path spelling walks around the self-mod gate #43). Merge order is still forced, now carrying three entries.

Footnote the gate earned

Staging this very comment was blocked by Lotor — the body quotes protected path names as prose, which is limit 26's class (the matcher scanning prose as code). Fourth block this run, all four handled with the right tool rather than by reshaping the command. The gate was not wrong to stop it; a comment body is an opaque string to a matcher.

Sign up for freeto 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

@githubscum
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

limits 63 + 64: what the version stamps cover, and where they reach - #44

Open
githubscum wants to merge 2 commits into
lotor-lane/limit-62-separator-spellingsfrom
lotor-lane/limit-63-matcher-stamp-blind
Open

limits 63 + 64: what the version stamps cover, and where they reach#44
githubscum wants to merge 2 commits into
lotor-lane/limit-62-separator-spellingsfrom
lotor-lane/limit-63-matcher-stamp-blind

Conversation

@githubscum

Copy link
Copy Markdown
Owner

Stacked on #43. Base is lotor-lane/limit-62-separator-spellings, not main, so the numbering does not collide (that branch adds limit 62; this adds 63). Merge #43 first and GitHub retargets this to main. The diff here is two files: one new test, one appended disclosure.

The work order

Verify what matcherVersionHash() actually covers, rather than what its docstring claims.

The defect

Every gated-action, policy-warn, grant and egress receipt carries a matcher version. It is the field a reader uses to answer the only question that makes two receipts comparable: were these decided by the same rules? The docstring calls it the "content hash of the matcher logic in force right now."

It hashes thirteen top-level functions plus RULE_TABLE and RULE_INFO. Function.prototype.toString() returns a function's own source and nothing it calls, so a helper is covered only if parts names it. The self-mod deciders are not named:

  • selfModFragmentsForBase — the protected-path list itself
  • isSelfModEdit, selfModCommandHit — both matcher bodies
  • normalizePath, pathContainsFragment — how a path is folded before matching
  • expandBraces, stripHeredocBodies, stripMessageArgs

isSelfModis hashed, and is a three-line dispatcher that names the two matchers and contains neither.

The consequence. Add a directory to the protected list, or change how a path is folded, and the gate stops a different set of actions while the stamp stays byte-identical. Two receipts either side of that change agree on the matcher version and disagree on the behaviour. The silence runs the wrong way: a matcher weakened between two runs keeps stamping the old, stronger version.

Enforcement is unaffected. The gate still gates correctly; the record misdescribes it. That is a witness defect rather than an enforcement one, which is why it gets its own entry rather than a line under limit 62.

What was tested

  • test/policy-matcher-stamp-coverage.test.js, 8 assertions. Two controls (the hashed bodies are present; the stamp is a stable 16-hex digest under matcher/1) so the absence assertions cannot pass vacuously by an import dying or a rename emptying the strings. Six assert the current defective state.
  • Full suite: 962 pass / 0 fail, up from 954 on the base branch.
  • Stamp on this build: matcher/1 95291ff6385151ca.
  • The introduction date was read from history (b1b7bf8, 2026-08-09), not inferred.

This is the tripwire pattern from #41 and #43: it asserts the defect, so it fails when someone fixes the stamp. The repair inverts the assertions and amends the disclosure in the same change. Deleting the block closes the confession by destroying its evidence.

What a reviewer should doubt

  1. The subset problem. The test reconstructs the hashed text from the eight matchers this module exports. Five more values in parts are not exported and are absent from my reconstruction. That is sound for proving absence (a string missing from the whole is missing from the subset) and would be unsound for proving presence — the controls only prove presence for strings I take from exported bodies. Check parts at the top of matcherVersionHash() yourself; the claim rests on reading that array, and the test is the mechanical half.
  2. I did not demonstrate two builds colliding. The direct proof is to mutate a fragment and show the digest unchanged. That mutation is src/policy and the gate refused it unsigned, correctly, so the evidence here is by construction plus the 2026-08-23 historical case, not by a hash collision I produced. If you want the stronger demonstration it costs one signature.
  3. Whether matcher/2 is the right repair. Bumping the schema marker on a coverage change is my reading of what that marker is for: the hashing method changes, and a value moving on its own would be indistinguishable from an ordinary rule edit. Arguable the other way.
  4. The KNOWN-LIMITS pin was not re-stamped. It still names 2173d231, same as on main and on limit 62: an equivalent path spelling walks around the self-mod gate #43. Re-stamping mid-branch would claim the log describes a commit that is not merged. No test enforces it; flagging so it is a choice rather than an oversight.

Scope

KNOWN-LIMITS.md and test/ only. Nothing in the non-delegable core is touched. The fix is core and queues for a signing sitting — it is named in the disclosure and deliberately not attempted here.

🤖 Generated with Claude Code

githubscumand others added 2 commits September 2, 2026 00:45
…deciders
Every gate, warn, grant and egress receipt carries a matcher version, and it
is the field a reader uses to ask whether two decisions were made by the same
rules. It hashes thirteen top-level functions plus the two rule tables.
Function.toString returns a function's own source and nothing it calls, so the
helpers that actually decide self-mod are outside it: the protected-path list,
both self-mod matchers, the path normalizer, the segment matcher, the brace
expander and the prose strippers. The dispatcher that IS hashed only names the
matchers it calls.
Change the protected list or the way a path is folded before matching, and the
gate stops a different set of actions while the stamp stays byte-identical.
The silence runs the wrong way: a matcher weakened between two runs keeps
stamping the old, stronger version. Enforcement is unaffected; the trace
misdescribes it, which is why this is filed as a witness defect.
Measured, not read off the source. The new test asserts the absence directly
against the hashed inputs, with controls asserting the hashed bodies are
present so the block cannot pass vacuously. Stamp on this build:
matcher/1 95291ff6385151ca. The stamp was introduced 2026-08-09 (b1b7bf8);
the protected list gained an entry on 2026-08-23 and the version did not move.
The repair is core and is not attempted here: name the helpers in parts and
bump the schema marker to matcher/2, since the hashing method changes and a
value moving on its own would be indistinguishable from a rule edit.
Suite 962 pass / 0 fail (8 new).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…record
Following limit 63's stated residual ("behavior that reaches the decision
from outside the policy module would remain unstamped") to where it leads,
and finding that the instrument which closes it is already built.
Two code identities exist. matcherVersionHash() covers named functions in
src/policy/index.js and is on every receipt. computeSourceDigest() covers
all 50 .js files under src/ and bin/ - including src/gate, src/grant,
src/chain, src/store and bin/hook-pre-tool-use.js - and is attached only to
MCP tool responses, which are discarded when the call returns.
So the ephemeral artifact carries the strong identity and the permanent one
carries the weak one. Change the gate or the hooks and every receipt after
the change is byte-comparable with every receipt before it.
Measured: build digest 47ed7876d2652e68 over 50 files / 522,651 bytes;
matcher stamp matcher/1 95291ff6385151ca; exactly two consumers of the
digest in the tree, neither of which writes to the chain.
Ships KNOWN-LIMITS 64 plus a 4-assertion test. Every absence claim is
paired with a control (the same file IS asserted to carry matcherHash, and
the MCP server IS asserted to carry the build stamp) so the block cannot
pass vacuously. Suite 966/966, up from 962.
The repair touches src/gate and bin/hook-* and therefore queues for a
signing sitting rather than riding along with this disclosure.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@githubscumgithubscum changed the title limit 63: the matcher version stamp hashes the entry points, not the deciderslimits 63 + 64: what the version stamps cover, and where they reachSep 2, 2026
@githubscum

Copy link
Copy Markdown
OwnerAuthor

Added in e60aa1e: KNOWN-LIMITS 64

This branch was opened for limit 63 alone. A second entry landed on it because it is the same subject and 63's own text points straight at it. Retitled accordingly. If you would rather review 63 by itself, say so and I will split 64 onto its own branch — nothing else depends on the pairing.

What 64 says

Limit 63 ends with a residual: "behavior that reaches the decision from outside [the policy module] would remain unstamped." It treats that as an accepted ceiling. It is not a ceiling. The instrument that closes it is already in this repository.

StampCoversReaches
matcherVersionHash()named functions in the policy moduleevery receipt
computeSourceDigest()all 50 .js files under the two source directories — including the gate, the grant checker, the chain writer, the store, and the pre-tool-use hookMCP tool responses only

The ephemeral artifact carries the strong identity; the permanent one carries the weak identity. Change the gate or the hooks, and every receipt written after is byte-comparable with every receipt written before.

Measured

  • build digest 47ed7876d2652e68, 50 files / 522,651 bytes
  • matcher stamp matcher/1 95291ff6385151ca (same value limit 63 recorded — consistent)
  • exactly two consumers of the digest in the whole tree, both inside the MCP directory. Neither writes to the chain.

Tested

test/stamp-reach-coverage.test.js, 4 assertions. Every absence claim is paired with a control — the same files are asserted to carrymatcherHash, and the MCP server is asserted to carry the build stamp — so the block cannot pass vacuously. Suite 966/966, up from 962 at 94bbc9a.

What a reviewer should doubt

  1. This is a disclosure, not a fix. The repair (carry the digest onto session-open, inherit by session id) touches non-delegable core. It queues for a signing sitting. Nothing in this PR changes gate behavior.
  2. "Exactly two consumers" is a regex over source text, not a call-graph analysis. A consumer reaching the digest by some indirection would not be caught. I read both files; I did not prove the negative mechanically.
  3. Whether session-open is the right carrier is a design call I made, not a measured result. Per-action stamping is more precise and costs a field on every receipt. I chose the cheap end. Argue with it.
  4. The severity depends on a premise worth checking: that gate, chain, and hook code changes often enough to matter. If that code is effectively frozen, this is theoretical. Eleven merged lane PRs suggest it is not frozen, but that is an inference, not a measurement.
  5. Stacking unchanged: this branch still sits on lotor-lane/limit-62-separator-spellings (limit 62: an equivalent path spelling walks around the self-mod gate #43). Merge order is still forced, now carrying three entries.

Footnote the gate earned

Staging this very comment was blocked by Lotor — the body quotes protected path names as prose, which is limit 26's class (the matcher scanning prose as code). Fourth block this run, all four handled with the right tool rather than by reshaping the command. The gate was not wrong to stop it; a comment body is an opaque string to a matcher.

Sign up for freeto 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

@githubscum
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

limits 63 + 64: what the version stamps cover, and where they reach - #44

Open
githubscum wants to merge 2 commits into
lotor-lane/limit-62-separator-spellingsfrom
lotor-lane/limit-63-matcher-stamp-blind
Open

limits 63 + 64: what the version stamps cover, and where they reach#44
githubscum wants to merge 2 commits into
lotor-lane/limit-62-separator-spellingsfrom
lotor-lane/limit-63-matcher-stamp-blind

Conversation

@githubscum

Copy link
Copy Markdown
Owner

Stacked on #43. Base is lotor-lane/limit-62-separator-spellings, not main, so the numbering does not collide (that branch adds limit 62; this adds 63). Merge #43 first and GitHub retargets this to main. The diff here is two files: one new test, one appended disclosure.

The work order

Verify what matcherVersionHash() actually covers, rather than what its docstring claims.

The defect

Every gated-action, policy-warn, grant and egress receipt carries a matcher version. It is the field a reader uses to answer the only question that makes two receipts comparable: were these decided by the same rules? The docstring calls it the "content hash of the matcher logic in force right now."

It hashes thirteen top-level functions plus RULE_TABLE and RULE_INFO. Function.prototype.toString() returns a function's own source and nothing it calls, so a helper is covered only if parts names it. The self-mod deciders are not named:

  • selfModFragmentsForBase — the protected-path list itself
  • isSelfModEdit, selfModCommandHit — both matcher bodies
  • normalizePath, pathContainsFragment — how a path is folded before matching
  • expandBraces, stripHeredocBodies, stripMessageArgs

isSelfModis hashed, and is a three-line dispatcher that names the two matchers and contains neither.

The consequence. Add a directory to the protected list, or change how a path is folded, and the gate stops a different set of actions while the stamp stays byte-identical. Two receipts either side of that change agree on the matcher version and disagree on the behaviour. The silence runs the wrong way: a matcher weakened between two runs keeps stamping the old, stronger version.

Enforcement is unaffected. The gate still gates correctly; the record misdescribes it. That is a witness defect rather than an enforcement one, which is why it gets its own entry rather than a line under limit 62.

What was tested

  • test/policy-matcher-stamp-coverage.test.js, 8 assertions. Two controls (the hashed bodies are present; the stamp is a stable 16-hex digest under matcher/1) so the absence assertions cannot pass vacuously by an import dying or a rename emptying the strings. Six assert the current defective state.
  • Full suite: 962 pass / 0 fail, up from 954 on the base branch.
  • Stamp on this build: matcher/1 95291ff6385151ca.
  • The introduction date was read from history (b1b7bf8, 2026-08-09), not inferred.

This is the tripwire pattern from #41 and #43: it asserts the defect, so it fails when someone fixes the stamp. The repair inverts the assertions and amends the disclosure in the same change. Deleting the block closes the confession by destroying its evidence.

What a reviewer should doubt

  1. The subset problem. The test reconstructs the hashed text from the eight matchers this module exports. Five more values in parts are not exported and are absent from my reconstruction. That is sound for proving absence (a string missing from the whole is missing from the subset) and would be unsound for proving presence — the controls only prove presence for strings I take from exported bodies. Check parts at the top of matcherVersionHash() yourself; the claim rests on reading that array, and the test is the mechanical half.
  2. I did not demonstrate two builds colliding. The direct proof is to mutate a fragment and show the digest unchanged. That mutation is src/policy and the gate refused it unsigned, correctly, so the evidence here is by construction plus the 2026-08-23 historical case, not by a hash collision I produced. If you want the stronger demonstration it costs one signature.
  3. Whether matcher/2 is the right repair. Bumping the schema marker on a coverage change is my reading of what that marker is for: the hashing method changes, and a value moving on its own would be indistinguishable from an ordinary rule edit. Arguable the other way.
  4. The KNOWN-LIMITS pin was not re-stamped. It still names 2173d231, same as on main and on limit 62: an equivalent path spelling walks around the self-mod gate #43. Re-stamping mid-branch would claim the log describes a commit that is not merged. No test enforces it; flagging so it is a choice rather than an oversight.

Scope

KNOWN-LIMITS.md and test/ only. Nothing in the non-delegable core is touched. The fix is core and queues for a signing sitting — it is named in the disclosure and deliberately not attempted here.

🤖 Generated with Claude Code

githubscumand others added 2 commits September 2, 2026 00:45
…deciders
Every gate, warn, grant and egress receipt carries a matcher version, and it
is the field a reader uses to ask whether two decisions were made by the same
rules. It hashes thirteen top-level functions plus the two rule tables.
Function.toString returns a function's own source and nothing it calls, so the
helpers that actually decide self-mod are outside it: the protected-path list,
both self-mod matchers, the path normalizer, the segment matcher, the brace
expander and the prose strippers. The dispatcher that IS hashed only names the
matchers it calls.
Change the protected list or the way a path is folded before matching, and the
gate stops a different set of actions while the stamp stays byte-identical.
The silence runs the wrong way: a matcher weakened between two runs keeps
stamping the old, stronger version. Enforcement is unaffected; the trace
misdescribes it, which is why this is filed as a witness defect.
Measured, not read off the source. The new test asserts the absence directly
against the hashed inputs, with controls asserting the hashed bodies are
present so the block cannot pass vacuously. Stamp on this build:
matcher/1 95291ff6385151ca. The stamp was introduced 2026-08-09 (b1b7bf8);
the protected list gained an entry on 2026-08-23 and the version did not move.
The repair is core and is not attempted here: name the helpers in parts and
bump the schema marker to matcher/2, since the hashing method changes and a
value moving on its own would be indistinguishable from a rule edit.
Suite 962 pass / 0 fail (8 new).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…record
Following limit 63's stated residual ("behavior that reaches the decision
from outside the policy module would remain unstamped") to where it leads,
and finding that the instrument which closes it is already built.
Two code identities exist. matcherVersionHash() covers named functions in
src/policy/index.js and is on every receipt. computeSourceDigest() covers
all 50 .js files under src/ and bin/ - including src/gate, src/grant,
src/chain, src/store and bin/hook-pre-tool-use.js - and is attached only to
MCP tool responses, which are discarded when the call returns.
So the ephemeral artifact carries the strong identity and the permanent one
carries the weak one. Change the gate or the hooks and every receipt after
the change is byte-comparable with every receipt before it.
Measured: build digest 47ed7876d2652e68 over 50 files / 522,651 bytes;
matcher stamp matcher/1 95291ff6385151ca; exactly two consumers of the
digest in the tree, neither of which writes to the chain.
Ships KNOWN-LIMITS 64 plus a 4-assertion test. Every absence claim is
paired with a control (the same file IS asserted to carry matcherHash, and
the MCP server IS asserted to carry the build stamp) so the block cannot
pass vacuously. Suite 966/966, up from 962.
The repair touches src/gate and bin/hook-* and therefore queues for a
signing sitting rather than riding along with this disclosure.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@githubscumgithubscum changed the title limit 63: the matcher version stamp hashes the entry points, not the deciderslimits 63 + 64: what the version stamps cover, and where they reachSep 2, 2026
@githubscum

Copy link
Copy Markdown
OwnerAuthor

Added in e60aa1e: KNOWN-LIMITS 64

This branch was opened for limit 63 alone. A second entry landed on it because it is the same subject and 63's own text points straight at it. Retitled accordingly. If you would rather review 63 by itself, say so and I will split 64 onto its own branch — nothing else depends on the pairing.

What 64 says

Limit 63 ends with a residual: "behavior that reaches the decision from outside [the policy module] would remain unstamped." It treats that as an accepted ceiling. It is not a ceiling. The instrument that closes it is already in this repository.

StampCoversReaches
matcherVersionHash()named functions in the policy moduleevery receipt
computeSourceDigest()all 50 .js files under the two source directories — including the gate, the grant checker, the chain writer, the store, and the pre-tool-use hookMCP tool responses only

The ephemeral artifact carries the strong identity; the permanent one carries the weak identity. Change the gate or the hooks, and every receipt written after is byte-comparable with every receipt written before.

Measured

  • build digest 47ed7876d2652e68, 50 files / 522,651 bytes
  • matcher stamp matcher/1 95291ff6385151ca (same value limit 63 recorded — consistent)
  • exactly two consumers of the digest in the whole tree, both inside the MCP directory. Neither writes to the chain.

Tested

test/stamp-reach-coverage.test.js, 4 assertions. Every absence claim is paired with a control — the same files are asserted to carrymatcherHash, and the MCP server is asserted to carry the build stamp — so the block cannot pass vacuously. Suite 966/966, up from 962 at 94bbc9a.

What a reviewer should doubt

  1. This is a disclosure, not a fix. The repair (carry the digest onto session-open, inherit by session id) touches non-delegable core. It queues for a signing sitting. Nothing in this PR changes gate behavior.
  2. "Exactly two consumers" is a regex over source text, not a call-graph analysis. A consumer reaching the digest by some indirection would not be caught. I read both files; I did not prove the negative mechanically.
  3. Whether session-open is the right carrier is a design call I made, not a measured result. Per-action stamping is more precise and costs a field on every receipt. I chose the cheap end. Argue with it.
  4. The severity depends on a premise worth checking: that gate, chain, and hook code changes often enough to matter. If that code is effectively frozen, this is theoretical. Eleven merged lane PRs suggest it is not frozen, but that is an inference, not a measurement.
  5. Stacking unchanged: this branch still sits on lotor-lane/limit-62-separator-spellings (limit 62: an equivalent path spelling walks around the self-mod gate #43). Merge order is still forced, now carrying three entries.

Footnote the gate earned

Staging this very comment was blocked by Lotor — the body quotes protected path names as prose, which is limit 26's class (the matcher scanning prose as code). Fourth block this run, all four handled with the right tool rather than by reshaping the command. The gate was not wrong to stop it; a comment body is an opaque string to a matcher.

Sign up for freeto 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

@githubscum
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

limits 63 + 64: what the version stamps cover, and where they reach - #44

Open
githubscum wants to merge 2 commits into
lotor-lane/limit-62-separator-spellingsfrom
lotor-lane/limit-63-matcher-stamp-blind
Open

limits 63 + 64: what the version stamps cover, and where they reach#44
githubscum wants to merge 2 commits into
lotor-lane/limit-62-separator-spellingsfrom
lotor-lane/limit-63-matcher-stamp-blind

Conversation

@githubscum

Copy link
Copy Markdown
Owner

Stacked on #43. Base is lotor-lane/limit-62-separator-spellings, not main, so the numbering does not collide (that branch adds limit 62; this adds 63). Merge #43 first and GitHub retargets this to main. The diff here is two files: one new test, one appended disclosure.

The work order

Verify what matcherVersionHash() actually covers, rather than what its docstring claims.

The defect

Every gated-action, policy-warn, grant and egress receipt carries a matcher version. It is the field a reader uses to answer the only question that makes two receipts comparable: were these decided by the same rules? The docstring calls it the "content hash of the matcher logic in force right now."

It hashes thirteen top-level functions plus RULE_TABLE and RULE_INFO. Function.prototype.toString() returns a function's own source and nothing it calls, so a helper is covered only if parts names it. The self-mod deciders are not named:

  • selfModFragmentsForBase — the protected-path list itself
  • isSelfModEdit, selfModCommandHit — both matcher bodies
  • normalizePath, pathContainsFragment — how a path is folded before matching
  • expandBraces, stripHeredocBodies, stripMessageArgs

isSelfModis hashed, and is a three-line dispatcher that names the two matchers and contains neither.

The consequence. Add a directory to the protected list, or change how a path is folded, and the gate stops a different set of actions while the stamp stays byte-identical. Two receipts either side of that change agree on the matcher version and disagree on the behaviour. The silence runs the wrong way: a matcher weakened between two runs keeps stamping the old, stronger version.

Enforcement is unaffected. The gate still gates correctly; the record misdescribes it. That is a witness defect rather than an enforcement one, which is why it gets its own entry rather than a line under limit 62.

What was tested

  • test/policy-matcher-stamp-coverage.test.js, 8 assertions. Two controls (the hashed bodies are present; the stamp is a stable 16-hex digest under matcher/1) so the absence assertions cannot pass vacuously by an import dying or a rename emptying the strings. Six assert the current defective state.
  • Full suite: 962 pass / 0 fail, up from 954 on the base branch.
  • Stamp on this build: matcher/1 95291ff6385151ca.
  • The introduction date was read from history (b1b7bf8, 2026-08-09), not inferred.

This is the tripwire pattern from #41 and #43: it asserts the defect, so it fails when someone fixes the stamp. The repair inverts the assertions and amends the disclosure in the same change. Deleting the block closes the confession by destroying its evidence.

What a reviewer should doubt

  1. The subset problem. The test reconstructs the hashed text from the eight matchers this module exports. Five more values in parts are not exported and are absent from my reconstruction. That is sound for proving absence (a string missing from the whole is missing from the subset) and would be unsound for proving presence — the controls only prove presence for strings I take from exported bodies. Check parts at the top of matcherVersionHash() yourself; the claim rests on reading that array, and the test is the mechanical half.
  2. I did not demonstrate two builds colliding. The direct proof is to mutate a fragment and show the digest unchanged. That mutation is src/policy and the gate refused it unsigned, correctly, so the evidence here is by construction plus the 2026-08-23 historical case, not by a hash collision I produced. If you want the stronger demonstration it costs one signature.
  3. Whether matcher/2 is the right repair. Bumping the schema marker on a coverage change is my reading of what that marker is for: the hashing method changes, and a value moving on its own would be indistinguishable from an ordinary rule edit. Arguable the other way.
  4. The KNOWN-LIMITS pin was not re-stamped. It still names 2173d231, same as on main and on limit 62: an equivalent path spelling walks around the self-mod gate #43. Re-stamping mid-branch would claim the log describes a commit that is not merged. No test enforces it; flagging so it is a choice rather than an oversight.

Scope

KNOWN-LIMITS.md and test/ only. Nothing in the non-delegable core is touched. The fix is core and queues for a signing sitting — it is named in the disclosure and deliberately not attempted here.

🤖 Generated with Claude Code

githubscumand others added 2 commits September 2, 2026 00:45
…deciders
Every gate, warn, grant and egress receipt carries a matcher version, and it
is the field a reader uses to ask whether two decisions were made by the same
rules. It hashes thirteen top-level functions plus the two rule tables.
Function.toString returns a function's own source and nothing it calls, so the
helpers that actually decide self-mod are outside it: the protected-path list,
both self-mod matchers, the path normalizer, the segment matcher, the brace
expander and the prose strippers. The dispatcher that IS hashed only names the
matchers it calls.
Change the protected list or the way a path is folded before matching, and the
gate stops a different set of actions while the stamp stays byte-identical.
The silence runs the wrong way: a matcher weakened between two runs keeps
stamping the old, stronger version. Enforcement is unaffected; the trace
misdescribes it, which is why this is filed as a witness defect.
Measured, not read off the source. The new test asserts the absence directly
against the hashed inputs, with controls asserting the hashed bodies are
present so the block cannot pass vacuously. Stamp on this build:
matcher/1 95291ff6385151ca. The stamp was introduced 2026-08-09 (b1b7bf8);
the protected list gained an entry on 2026-08-23 and the version did not move.
The repair is core and is not attempted here: name the helpers in parts and
bump the schema marker to matcher/2, since the hashing method changes and a
value moving on its own would be indistinguishable from a rule edit.
Suite 962 pass / 0 fail (8 new).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…record
Following limit 63's stated residual ("behavior that reaches the decision
from outside the policy module would remain unstamped") to where it leads,
and finding that the instrument which closes it is already built.
Two code identities exist. matcherVersionHash() covers named functions in
src/policy/index.js and is on every receipt. computeSourceDigest() covers
all 50 .js files under src/ and bin/ - including src/gate, src/grant,
src/chain, src/store and bin/hook-pre-tool-use.js - and is attached only to
MCP tool responses, which are discarded when the call returns.
So the ephemeral artifact carries the strong identity and the permanent one
carries the weak one. Change the gate or the hooks and every receipt after
the change is byte-comparable with every receipt before it.
Measured: build digest 47ed7876d2652e68 over 50 files / 522,651 bytes;
matcher stamp matcher/1 95291ff6385151ca; exactly two consumers of the
digest in the tree, neither of which writes to the chain.
Ships KNOWN-LIMITS 64 plus a 4-assertion test. Every absence claim is
paired with a control (the same file IS asserted to carry matcherHash, and
the MCP server IS asserted to carry the build stamp) so the block cannot
pass vacuously. Suite 966/966, up from 962.
The repair touches src/gate and bin/hook-* and therefore queues for a
signing sitting rather than riding along with this disclosure.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@githubscumgithubscum changed the title limit 63: the matcher version stamp hashes the entry points, not the deciderslimits 63 + 64: what the version stamps cover, and where they reachSep 2, 2026
@githubscum

Copy link
Copy Markdown
OwnerAuthor

Added in e60aa1e: KNOWN-LIMITS 64

This branch was opened for limit 63 alone. A second entry landed on it because it is the same subject and 63's own text points straight at it. Retitled accordingly. If you would rather review 63 by itself, say so and I will split 64 onto its own branch — nothing else depends on the pairing.

What 64 says

Limit 63 ends with a residual: "behavior that reaches the decision from outside [the policy module] would remain unstamped." It treats that as an accepted ceiling. It is not a ceiling. The instrument that closes it is already in this repository.

StampCoversReaches
matcherVersionHash()named functions in the policy moduleevery receipt
computeSourceDigest()all 50 .js files under the two source directories — including the gate, the grant checker, the chain writer, the store, and the pre-tool-use hookMCP tool responses only

The ephemeral artifact carries the strong identity; the permanent one carries the weak identity. Change the gate or the hooks, and every receipt written after is byte-comparable with every receipt written before.

Measured

  • build digest 47ed7876d2652e68, 50 files / 522,651 bytes
  • matcher stamp matcher/1 95291ff6385151ca (same value limit 63 recorded — consistent)
  • exactly two consumers of the digest in the whole tree, both inside the MCP directory. Neither writes to the chain.

Tested

test/stamp-reach-coverage.test.js, 4 assertions. Every absence claim is paired with a control — the same files are asserted to carrymatcherHash, and the MCP server is asserted to carry the build stamp — so the block cannot pass vacuously. Suite 966/966, up from 962 at 94bbc9a.

What a reviewer should doubt

  1. This is a disclosure, not a fix. The repair (carry the digest onto session-open, inherit by session id) touches non-delegable core. It queues for a signing sitting. Nothing in this PR changes gate behavior.
  2. "Exactly two consumers" is a regex over source text, not a call-graph analysis. A consumer reaching the digest by some indirection would not be caught. I read both files; I did not prove the negative mechanically.
  3. Whether session-open is the right carrier is a design call I made, not a measured result. Per-action stamping is more precise and costs a field on every receipt. I chose the cheap end. Argue with it.
  4. The severity depends on a premise worth checking: that gate, chain, and hook code changes often enough to matter. If that code is effectively frozen, this is theoretical. Eleven merged lane PRs suggest it is not frozen, but that is an inference, not a measurement.
  5. Stacking unchanged: this branch still sits on lotor-lane/limit-62-separator-spellings (limit 62: an equivalent path spelling walks around the self-mod gate #43). Merge order is still forced, now carrying three entries.

Footnote the gate earned

Staging this very comment was blocked by Lotor — the body quotes protected path names as prose, which is limit 26's class (the matcher scanning prose as code). Fourth block this run, all four handled with the right tool rather than by reshaping the command. The gate was not wrong to stop it; a comment body is an opaque string to a matcher.

Sign up for freeto 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

@githubscum
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

limits 63 + 64: what the version stamps cover, and where they reach - #44

Open
githubscum wants to merge 2 commits into
lotor-lane/limit-62-separator-spellingsfrom
lotor-lane/limit-63-matcher-stamp-blind
Open

limits 63 + 64: what the version stamps cover, and where they reach#44
githubscum wants to merge 2 commits into
lotor-lane/limit-62-separator-spellingsfrom
lotor-lane/limit-63-matcher-stamp-blind

Conversation

@githubscum

Copy link
Copy Markdown
Owner

Stacked on #43. Base is lotor-lane/limit-62-separator-spellings, not main, so the numbering does not collide (that branch adds limit 62; this adds 63). Merge #43 first and GitHub retargets this to main. The diff here is two files: one new test, one appended disclosure.

The work order

Verify what matcherVersionHash() actually covers, rather than what its docstring claims.

The defect

Every gated-action, policy-warn, grant and egress receipt carries a matcher version. It is the field a reader uses to answer the only question that makes two receipts comparable: were these decided by the same rules? The docstring calls it the "content hash of the matcher logic in force right now."

It hashes thirteen top-level functions plus RULE_TABLE and RULE_INFO. Function.prototype.toString() returns a function's own source and nothing it calls, so a helper is covered only if parts names it. The self-mod deciders are not named:

  • selfModFragmentsForBase — the protected-path list itself
  • isSelfModEdit, selfModCommandHit — both matcher bodies
  • normalizePath, pathContainsFragment — how a path is folded before matching
  • expandBraces, stripHeredocBodies, stripMessageArgs

isSelfModis hashed, and is a three-line dispatcher that names the two matchers and contains neither.

The consequence. Add a directory to the protected list, or change how a path is folded, and the gate stops a different set of actions while the stamp stays byte-identical. Two receipts either side of that change agree on the matcher version and disagree on the behaviour. The silence runs the wrong way: a matcher weakened between two runs keeps stamping the old, stronger version.

Enforcement is unaffected. The gate still gates correctly; the record misdescribes it. That is a witness defect rather than an enforcement one, which is why it gets its own entry rather than a line under limit 62.

What was tested

  • test/policy-matcher-stamp-coverage.test.js, 8 assertions. Two controls (the hashed bodies are present; the stamp is a stable 16-hex digest under matcher/1) so the absence assertions cannot pass vacuously by an import dying or a rename emptying the strings. Six assert the current defective state.
  • Full suite: 962 pass / 0 fail, up from 954 on the base branch.
  • Stamp on this build: matcher/1 95291ff6385151ca.
  • The introduction date was read from history (b1b7bf8, 2026-08-09), not inferred.

This is the tripwire pattern from #41 and #43: it asserts the defect, so it fails when someone fixes the stamp. The repair inverts the assertions and amends the disclosure in the same change. Deleting the block closes the confession by destroying its evidence.

What a reviewer should doubt

  1. The subset problem. The test reconstructs the hashed text from the eight matchers this module exports. Five more values in parts are not exported and are absent from my reconstruction. That is sound for proving absence (a string missing from the whole is missing from the subset) and would be unsound for proving presence — the controls only prove presence for strings I take from exported bodies. Check parts at the top of matcherVersionHash() yourself; the claim rests on reading that array, and the test is the mechanical half.
  2. I did not demonstrate two builds colliding. The direct proof is to mutate a fragment and show the digest unchanged. That mutation is src/policy and the gate refused it unsigned, correctly, so the evidence here is by construction plus the 2026-08-23 historical case, not by a hash collision I produced. If you want the stronger demonstration it costs one signature.
  3. Whether matcher/2 is the right repair. Bumping the schema marker on a coverage change is my reading of what that marker is for: the hashing method changes, and a value moving on its own would be indistinguishable from an ordinary rule edit. Arguable the other way.
  4. The KNOWN-LIMITS pin was not re-stamped. It still names 2173d231, same as on main and on limit 62: an equivalent path spelling walks around the self-mod gate #43. Re-stamping mid-branch would claim the log describes a commit that is not merged. No test enforces it; flagging so it is a choice rather than an oversight.

Scope

KNOWN-LIMITS.md and test/ only. Nothing in the non-delegable core is touched. The fix is core and queues for a signing sitting — it is named in the disclosure and deliberately not attempted here.

🤖 Generated with Claude Code

githubscumand others added 2 commits September 2, 2026 00:45
…deciders
Every gate, warn, grant and egress receipt carries a matcher version, and it
is the field a reader uses to ask whether two decisions were made by the same
rules. It hashes thirteen top-level functions plus the two rule tables.
Function.toString returns a function's own source and nothing it calls, so the
helpers that actually decide self-mod are outside it: the protected-path list,
both self-mod matchers, the path normalizer, the segment matcher, the brace
expander and the prose strippers. The dispatcher that IS hashed only names the
matchers it calls.
Change the protected list or the way a path is folded before matching, and the
gate stops a different set of actions while the stamp stays byte-identical.
The silence runs the wrong way: a matcher weakened between two runs keeps
stamping the old, stronger version. Enforcement is unaffected; the trace
misdescribes it, which is why this is filed as a witness defect.
Measured, not read off the source. The new test asserts the absence directly
against the hashed inputs, with controls asserting the hashed bodies are
present so the block cannot pass vacuously. Stamp on this build:
matcher/1 95291ff6385151ca. The stamp was introduced 2026-08-09 (b1b7bf8);
the protected list gained an entry on 2026-08-23 and the version did not move.
The repair is core and is not attempted here: name the helpers in parts and
bump the schema marker to matcher/2, since the hashing method changes and a
value moving on its own would be indistinguishable from a rule edit.
Suite 962 pass / 0 fail (8 new).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…record
Following limit 63's stated residual ("behavior that reaches the decision
from outside the policy module would remain unstamped") to where it leads,
and finding that the instrument which closes it is already built.
Two code identities exist. matcherVersionHash() covers named functions in
src/policy/index.js and is on every receipt. computeSourceDigest() covers
all 50 .js files under src/ and bin/ - including src/gate, src/grant,
src/chain, src/store and bin/hook-pre-tool-use.js - and is attached only to
MCP tool responses, which are discarded when the call returns.
So the ephemeral artifact carries the strong identity and the permanent one
carries the weak one. Change the gate or the hooks and every receipt after
the change is byte-comparable with every receipt before it.
Measured: build digest 47ed7876d2652e68 over 50 files / 522,651 bytes;
matcher stamp matcher/1 95291ff6385151ca; exactly two consumers of the
digest in the tree, neither of which writes to the chain.
Ships KNOWN-LIMITS 64 plus a 4-assertion test. Every absence claim is
paired with a control (the same file IS asserted to carry matcherHash, and
the MCP server IS asserted to carry the build stamp) so the block cannot
pass vacuously. Suite 966/966, up from 962.
The repair touches src/gate and bin/hook-* and therefore queues for a
signing sitting rather than riding along with this disclosure.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@githubscumgithubscum changed the title limit 63: the matcher version stamp hashes the entry points, not the deciderslimits 63 + 64: what the version stamps cover, and where they reachSep 2, 2026
@githubscum

Copy link
Copy Markdown
OwnerAuthor

Added in e60aa1e: KNOWN-LIMITS 64

This branch was opened for limit 63 alone. A second entry landed on it because it is the same subject and 63's own text points straight at it. Retitled accordingly. If you would rather review 63 by itself, say so and I will split 64 onto its own branch — nothing else depends on the pairing.

What 64 says

Limit 63 ends with a residual: "behavior that reaches the decision from outside [the policy module] would remain unstamped." It treats that as an accepted ceiling. It is not a ceiling. The instrument that closes it is already in this repository.

StampCoversReaches
matcherVersionHash()named functions in the policy moduleevery receipt
computeSourceDigest()all 50 .js files under the two source directories — including the gate, the grant checker, the chain writer, the store, and the pre-tool-use hookMCP tool responses only

The ephemeral artifact carries the strong identity; the permanent one carries the weak identity. Change the gate or the hooks, and every receipt written after is byte-comparable with every receipt written before.

Measured

  • build digest 47ed7876d2652e68, 50 files / 522,651 bytes
  • matcher stamp matcher/1 95291ff6385151ca (same value limit 63 recorded — consistent)
  • exactly two consumers of the digest in the whole tree, both inside the MCP directory. Neither writes to the chain.

Tested

test/stamp-reach-coverage.test.js, 4 assertions. Every absence claim is paired with a control — the same files are asserted to carrymatcherHash, and the MCP server is asserted to carry the build stamp — so the block cannot pass vacuously. Suite 966/966, up from 962 at 94bbc9a.

What a reviewer should doubt

  1. This is a disclosure, not a fix. The repair (carry the digest onto session-open, inherit by session id) touches non-delegable core. It queues for a signing sitting. Nothing in this PR changes gate behavior.
  2. "Exactly two consumers" is a regex over source text, not a call-graph analysis. A consumer reaching the digest by some indirection would not be caught. I read both files; I did not prove the negative mechanically.
  3. Whether session-open is the right carrier is a design call I made, not a measured result. Per-action stamping is more precise and costs a field on every receipt. I chose the cheap end. Argue with it.
  4. The severity depends on a premise worth checking: that gate, chain, and hook code changes often enough to matter. If that code is effectively frozen, this is theoretical. Eleven merged lane PRs suggest it is not frozen, but that is an inference, not a measurement.
  5. Stacking unchanged: this branch still sits on lotor-lane/limit-62-separator-spellings (limit 62: an equivalent path spelling walks around the self-mod gate #43). Merge order is still forced, now carrying three entries.

Footnote the gate earned

Staging this very comment was blocked by Lotor — the body quotes protected path names as prose, which is limit 26's class (the matcher scanning prose as code). Fourth block this run, all four handled with the right tool rather than by reshaping the command. The gate was not wrong to stop it; a comment body is an opaque string to a matcher.

Sign up for freeto 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

@githubscum