Egress capture: PostToolUse hook for what actually left the machine - #4

Merged
githubscum merged 1 commit into
mainfrom
feat/egress-capture
Jul 22, 2026
Merged

Egress capture: PostToolUse hook for what actually left the machine#4
githubscum merged 1 commit into
mainfrom
feat/egress-capture

Conversation

@githubscum

Copy link
Copy Markdown
Owner

Follows #3 (gated runs). Branched from feat/gated-runs since it reuses that branch's policy matchers; this diff will include #3's changes until #3 merges, which is expected for a stacked branch.

What this closes, and what it doesn't

KNOWN-LIMITS item 2 says outbound capture "is not fully derivable from Claude Code JSONL session transcripts alone" and names MCP-boundary instrumentation as the fix. This is a real step in that direction, not the whole thing, and the docs say so precisely rather than overselling it.

A PostToolUse hook fires after a tool call completes, with both the tool_input and the tool_response the host actually observed. That's captured live, at the moment of execution, by the host — not reconstructed later by parsing a transcript file. Genuine improvement in attestation strength. But it is not wire-level capture: no network proxy, no TLS interception, no independent check that a tool's own response was honest. A tool that lies about its own outcome, or an egress path the matchers don't recognize, isn't caught. True wire-level capture is a larger architectural change, not this PR.

What it does

bin/hook-post-tool-use.js reuses isPushForce, isPushProtected, isPublish, isEgressOther from src/policy/ unchanged — no duplicated matcher logic. On a match, appends an egress-event receipt carrying only digests: paramsDigest, responseDigest, and a best-effort responseOk signal extracted from common success/error shapes. Raw tool_input and raw tool_response are never written to the chain, matching the digest-only convention every other receipt in this repo follows.

No exit-2 path. PostToolUse fires after the call already completed, so there is nothing left to block; the hook always exits 0.

Verification

  • 177 tests passing (172 baseline + 5).
  • The work order's own demo proved no raw content in the receipt for a git push --force payload.
  • I went further in review: independently re-probed with a fabricated exfil URL and a distinctive secret string planted in tool_input/tool_response, then grepped the persisted chain entry directly. Zero hits on both. That's the check that actually matters for a receipt layer's core promise.

Views and docs

  • renderMorningAfter gains an EGRESS EVENTS block (total + per-rule breakdown), same style as the existing POLICY WARNINGS block from gated runs.
  • KNOWN-LIMITS item 2 rewritten to describe the new reality precisely: what's captured now, and what still needs a real network-boundary proxy that isn't in v1.

Real step toward "what actually left the machine" without overclaiming
wire-level capture. A PostToolUse hook fires after the host has observed
both tool_input and tool_response for a completed call. Attested at time
of execution by the host, not reconstructed later from a transcript,
which is a genuine step up over the JSONL-parsing baseline. Explicitly
not a network proxy: no TLS interception, no independent check that a
tool's own response was honest.
- bin/hook-post-tool-use.js: reuses src/policy's isPushForce /
isPushProtected / isPublish / isEgressOther unchanged (no duplicated
matcher logic). On a hit, appends an "egress-event" receipt carrying
only digests: paramsDigest, responseDigest, and a best-effort
responseOk signal. Raw tool_input and raw tool_response are never
written to the chain, matching the digest-only convention every other
receipt in this repo follows. No exit-2 path: PostToolUse cannot block
a call that already completed, so the hook always exits 0.
- views: EGRESS EVENTS block in the morning-after summary, same style
as the existing POLICY WARNINGS block.
- KNOWN-LIMITS 2 rewritten to describe the new reality precisely: what
is captured now, and what still requires a real network-boundary
proxy that is not in v1.
Tests: 177 passing (172 baseline + 5). Independently re-verified beyond
the work order's own demo: a fabricated exfil URL and a distinctive
secret string in tool_input/tool_response both produced zero hits when
grepped against the persisted chain entry.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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

Egress capture: PostToolUse hook for what actually left the machine - #4

Merged
githubscum merged 1 commit into
mainfrom
feat/egress-capture
Jul 22, 2026
Merged

Egress capture: PostToolUse hook for what actually left the machine#4
githubscum merged 1 commit into
mainfrom
feat/egress-capture

Conversation

@githubscum

Copy link
Copy Markdown
Owner

Follows #3 (gated runs). Branched from feat/gated-runs since it reuses that branch's policy matchers; this diff will include #3's changes until #3 merges, which is expected for a stacked branch.

What this closes, and what it doesn't

KNOWN-LIMITS item 2 says outbound capture "is not fully derivable from Claude Code JSONL session transcripts alone" and names MCP-boundary instrumentation as the fix. This is a real step in that direction, not the whole thing, and the docs say so precisely rather than overselling it.

A PostToolUse hook fires after a tool call completes, with both the tool_input and the tool_response the host actually observed. That's captured live, at the moment of execution, by the host — not reconstructed later by parsing a transcript file. Genuine improvement in attestation strength. But it is not wire-level capture: no network proxy, no TLS interception, no independent check that a tool's own response was honest. A tool that lies about its own outcome, or an egress path the matchers don't recognize, isn't caught. True wire-level capture is a larger architectural change, not this PR.

What it does

bin/hook-post-tool-use.js reuses isPushForce, isPushProtected, isPublish, isEgressOther from src/policy/ unchanged — no duplicated matcher logic. On a match, appends an egress-event receipt carrying only digests: paramsDigest, responseDigest, and a best-effort responseOk signal extracted from common success/error shapes. Raw tool_input and raw tool_response are never written to the chain, matching the digest-only convention every other receipt in this repo follows.

No exit-2 path. PostToolUse fires after the call already completed, so there is nothing left to block; the hook always exits 0.

Verification

  • 177 tests passing (172 baseline + 5).
  • The work order's own demo proved no raw content in the receipt for a git push --force payload.
  • I went further in review: independently re-probed with a fabricated exfil URL and a distinctive secret string planted in tool_input/tool_response, then grepped the persisted chain entry directly. Zero hits on both. That's the check that actually matters for a receipt layer's core promise.

Views and docs

  • renderMorningAfter gains an EGRESS EVENTS block (total + per-rule breakdown), same style as the existing POLICY WARNINGS block from gated runs.
  • KNOWN-LIMITS item 2 rewritten to describe the new reality precisely: what's captured now, and what still needs a real network-boundary proxy that isn't in v1.

Real step toward "what actually left the machine" without overclaiming
wire-level capture. A PostToolUse hook fires after the host has observed
both tool_input and tool_response for a completed call. Attested at time
of execution by the host, not reconstructed later from a transcript,
which is a genuine step up over the JSONL-parsing baseline. Explicitly
not a network proxy: no TLS interception, no independent check that a
tool's own response was honest.
- bin/hook-post-tool-use.js: reuses src/policy's isPushForce /
isPushProtected / isPublish / isEgressOther unchanged (no duplicated
matcher logic). On a hit, appends an "egress-event" receipt carrying
only digests: paramsDigest, responseDigest, and a best-effort
responseOk signal. Raw tool_input and raw tool_response are never
written to the chain, matching the digest-only convention every other
receipt in this repo follows. No exit-2 path: PostToolUse cannot block
a call that already completed, so the hook always exits 0.
- views: EGRESS EVENTS block in the morning-after summary, same style
as the existing POLICY WARNINGS block.
- KNOWN-LIMITS 2 rewritten to describe the new reality precisely: what
is captured now, and what still requires a real network-boundary
proxy that is not in v1.
Tests: 177 passing (172 baseline + 5). Independently re-verified beyond
the work order's own demo: a fabricated exfil URL and a distinctive
secret string in tool_input/tool_response both produced zero hits when
grepped against the persisted chain entry.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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

Egress capture: PostToolUse hook for what actually left the machine - #4

Merged
githubscum merged 1 commit into
mainfrom
feat/egress-capture
Jul 22, 2026
Merged

Egress capture: PostToolUse hook for what actually left the machine#4
githubscum merged 1 commit into
mainfrom
feat/egress-capture

Conversation

@githubscum

Copy link
Copy Markdown
Owner

Follows #3 (gated runs). Branched from feat/gated-runs since it reuses that branch's policy matchers; this diff will include #3's changes until #3 merges, which is expected for a stacked branch.

What this closes, and what it doesn't

KNOWN-LIMITS item 2 says outbound capture "is not fully derivable from Claude Code JSONL session transcripts alone" and names MCP-boundary instrumentation as the fix. This is a real step in that direction, not the whole thing, and the docs say so precisely rather than overselling it.

A PostToolUse hook fires after a tool call completes, with both the tool_input and the tool_response the host actually observed. That's captured live, at the moment of execution, by the host — not reconstructed later by parsing a transcript file. Genuine improvement in attestation strength. But it is not wire-level capture: no network proxy, no TLS interception, no independent check that a tool's own response was honest. A tool that lies about its own outcome, or an egress path the matchers don't recognize, isn't caught. True wire-level capture is a larger architectural change, not this PR.

What it does

bin/hook-post-tool-use.js reuses isPushForce, isPushProtected, isPublish, isEgressOther from src/policy/ unchanged — no duplicated matcher logic. On a match, appends an egress-event receipt carrying only digests: paramsDigest, responseDigest, and a best-effort responseOk signal extracted from common success/error shapes. Raw tool_input and raw tool_response are never written to the chain, matching the digest-only convention every other receipt in this repo follows.

No exit-2 path. PostToolUse fires after the call already completed, so there is nothing left to block; the hook always exits 0.

Verification

  • 177 tests passing (172 baseline + 5).
  • The work order's own demo proved no raw content in the receipt for a git push --force payload.
  • I went further in review: independently re-probed with a fabricated exfil URL and a distinctive secret string planted in tool_input/tool_response, then grepped the persisted chain entry directly. Zero hits on both. That's the check that actually matters for a receipt layer's core promise.

Views and docs

  • renderMorningAfter gains an EGRESS EVENTS block (total + per-rule breakdown), same style as the existing POLICY WARNINGS block from gated runs.
  • KNOWN-LIMITS item 2 rewritten to describe the new reality precisely: what's captured now, and what still needs a real network-boundary proxy that isn't in v1.

Real step toward "what actually left the machine" without overclaiming
wire-level capture. A PostToolUse hook fires after the host has observed
both tool_input and tool_response for a completed call. Attested at time
of execution by the host, not reconstructed later from a transcript,
which is a genuine step up over the JSONL-parsing baseline. Explicitly
not a network proxy: no TLS interception, no independent check that a
tool's own response was honest.
- bin/hook-post-tool-use.js: reuses src/policy's isPushForce /
isPushProtected / isPublish / isEgressOther unchanged (no duplicated
matcher logic). On a hit, appends an "egress-event" receipt carrying
only digests: paramsDigest, responseDigest, and a best-effort
responseOk signal. Raw tool_input and raw tool_response are never
written to the chain, matching the digest-only convention every other
receipt in this repo follows. No exit-2 path: PostToolUse cannot block
a call that already completed, so the hook always exits 0.
- views: EGRESS EVENTS block in the morning-after summary, same style
as the existing POLICY WARNINGS block.
- KNOWN-LIMITS 2 rewritten to describe the new reality precisely: what
is captured now, and what still requires a real network-boundary
proxy that is not in v1.
Tests: 177 passing (172 baseline + 5). Independently re-verified beyond
the work order's own demo: a fabricated exfil URL and a distinctive
secret string in tool_input/tool_response both produced zero hits when
grepped against the persisted chain entry.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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

Egress capture: PostToolUse hook for what actually left the machine - #4

Merged
githubscum merged 1 commit into
mainfrom
feat/egress-capture
Jul 22, 2026
Merged

Egress capture: PostToolUse hook for what actually left the machine#4
githubscum merged 1 commit into
mainfrom
feat/egress-capture

Conversation

@githubscum

Copy link
Copy Markdown
Owner

Follows #3 (gated runs). Branched from feat/gated-runs since it reuses that branch's policy matchers; this diff will include #3's changes until #3 merges, which is expected for a stacked branch.

What this closes, and what it doesn't

KNOWN-LIMITS item 2 says outbound capture "is not fully derivable from Claude Code JSONL session transcripts alone" and names MCP-boundary instrumentation as the fix. This is a real step in that direction, not the whole thing, and the docs say so precisely rather than overselling it.

A PostToolUse hook fires after a tool call completes, with both the tool_input and the tool_response the host actually observed. That's captured live, at the moment of execution, by the host — not reconstructed later by parsing a transcript file. Genuine improvement in attestation strength. But it is not wire-level capture: no network proxy, no TLS interception, no independent check that a tool's own response was honest. A tool that lies about its own outcome, or an egress path the matchers don't recognize, isn't caught. True wire-level capture is a larger architectural change, not this PR.

What it does

bin/hook-post-tool-use.js reuses isPushForce, isPushProtected, isPublish, isEgressOther from src/policy/ unchanged — no duplicated matcher logic. On a match, appends an egress-event receipt carrying only digests: paramsDigest, responseDigest, and a best-effort responseOk signal extracted from common success/error shapes. Raw tool_input and raw tool_response are never written to the chain, matching the digest-only convention every other receipt in this repo follows.

No exit-2 path. PostToolUse fires after the call already completed, so there is nothing left to block; the hook always exits 0.

Verification

  • 177 tests passing (172 baseline + 5).
  • The work order's own demo proved no raw content in the receipt for a git push --force payload.
  • I went further in review: independently re-probed with a fabricated exfil URL and a distinctive secret string planted in tool_input/tool_response, then grepped the persisted chain entry directly. Zero hits on both. That's the check that actually matters for a receipt layer's core promise.

Views and docs

  • renderMorningAfter gains an EGRESS EVENTS block (total + per-rule breakdown), same style as the existing POLICY WARNINGS block from gated runs.
  • KNOWN-LIMITS item 2 rewritten to describe the new reality precisely: what's captured now, and what still needs a real network-boundary proxy that isn't in v1.

Real step toward "what actually left the machine" without overclaiming
wire-level capture. A PostToolUse hook fires after the host has observed
both tool_input and tool_response for a completed call. Attested at time
of execution by the host, not reconstructed later from a transcript,
which is a genuine step up over the JSONL-parsing baseline. Explicitly
not a network proxy: no TLS interception, no independent check that a
tool's own response was honest.
- bin/hook-post-tool-use.js: reuses src/policy's isPushForce /
isPushProtected / isPublish / isEgressOther unchanged (no duplicated
matcher logic). On a hit, appends an "egress-event" receipt carrying
only digests: paramsDigest, responseDigest, and a best-effort
responseOk signal. Raw tool_input and raw tool_response are never
written to the chain, matching the digest-only convention every other
receipt in this repo follows. No exit-2 path: PostToolUse cannot block
a call that already completed, so the hook always exits 0.
- views: EGRESS EVENTS block in the morning-after summary, same style
as the existing POLICY WARNINGS block.
- KNOWN-LIMITS 2 rewritten to describe the new reality precisely: what
is captured now, and what still requires a real network-boundary
proxy that is not in v1.
Tests: 177 passing (172 baseline + 5). Independently re-verified beyond
the work order's own demo: a fabricated exfil URL and a distinctive
secret string in tool_input/tool_response both produced zero hits when
grepped against the persisted chain entry.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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

Egress capture: PostToolUse hook for what actually left the machine - #4

Merged
githubscum merged 1 commit into
mainfrom
feat/egress-capture
Jul 22, 2026
Merged

Egress capture: PostToolUse hook for what actually left the machine#4
githubscum merged 1 commit into
mainfrom
feat/egress-capture

Conversation

@githubscum

Copy link
Copy Markdown
Owner

Follows #3 (gated runs). Branched from feat/gated-runs since it reuses that branch's policy matchers; this diff will include #3's changes until #3 merges, which is expected for a stacked branch.

What this closes, and what it doesn't

KNOWN-LIMITS item 2 says outbound capture "is not fully derivable from Claude Code JSONL session transcripts alone" and names MCP-boundary instrumentation as the fix. This is a real step in that direction, not the whole thing, and the docs say so precisely rather than overselling it.

A PostToolUse hook fires after a tool call completes, with both the tool_input and the tool_response the host actually observed. That's captured live, at the moment of execution, by the host — not reconstructed later by parsing a transcript file. Genuine improvement in attestation strength. But it is not wire-level capture: no network proxy, no TLS interception, no independent check that a tool's own response was honest. A tool that lies about its own outcome, or an egress path the matchers don't recognize, isn't caught. True wire-level capture is a larger architectural change, not this PR.

What it does

bin/hook-post-tool-use.js reuses isPushForce, isPushProtected, isPublish, isEgressOther from src/policy/ unchanged — no duplicated matcher logic. On a match, appends an egress-event receipt carrying only digests: paramsDigest, responseDigest, and a best-effort responseOk signal extracted from common success/error shapes. Raw tool_input and raw tool_response are never written to the chain, matching the digest-only convention every other receipt in this repo follows.

No exit-2 path. PostToolUse fires after the call already completed, so there is nothing left to block; the hook always exits 0.

Verification

  • 177 tests passing (172 baseline + 5).
  • The work order's own demo proved no raw content in the receipt for a git push --force payload.
  • I went further in review: independently re-probed with a fabricated exfil URL and a distinctive secret string planted in tool_input/tool_response, then grepped the persisted chain entry directly. Zero hits on both. That's the check that actually matters for a receipt layer's core promise.

Views and docs

  • renderMorningAfter gains an EGRESS EVENTS block (total + per-rule breakdown), same style as the existing POLICY WARNINGS block from gated runs.
  • KNOWN-LIMITS item 2 rewritten to describe the new reality precisely: what's captured now, and what still needs a real network-boundary proxy that isn't in v1.

Real step toward "what actually left the machine" without overclaiming
wire-level capture. A PostToolUse hook fires after the host has observed
both tool_input and tool_response for a completed call. Attested at time
of execution by the host, not reconstructed later from a transcript,
which is a genuine step up over the JSONL-parsing baseline. Explicitly
not a network proxy: no TLS interception, no independent check that a
tool's own response was honest.
- bin/hook-post-tool-use.js: reuses src/policy's isPushForce /
isPushProtected / isPublish / isEgressOther unchanged (no duplicated
matcher logic). On a hit, appends an "egress-event" receipt carrying
only digests: paramsDigest, responseDigest, and a best-effort
responseOk signal. Raw tool_input and raw tool_response are never
written to the chain, matching the digest-only convention every other
receipt in this repo follows. No exit-2 path: PostToolUse cannot block
a call that already completed, so the hook always exits 0.
- views: EGRESS EVENTS block in the morning-after summary, same style
as the existing POLICY WARNINGS block.
- KNOWN-LIMITS 2 rewritten to describe the new reality precisely: what
is captured now, and what still requires a real network-boundary
proxy that is not in v1.
Tests: 177 passing (172 baseline + 5). Independently re-verified beyond
the work order's own demo: a fabricated exfil URL and a distinctive
secret string in tool_input/tool_response both produced zero hits when
grepped against the persisted chain entry.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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

Egress capture: PostToolUse hook for what actually left the machine - #4

Merged
githubscum merged 1 commit into
mainfrom
feat/egress-capture
Jul 22, 2026
Merged

Egress capture: PostToolUse hook for what actually left the machine#4
githubscum merged 1 commit into
mainfrom
feat/egress-capture

Conversation

@githubscum

Copy link
Copy Markdown
Owner

Follows #3 (gated runs). Branched from feat/gated-runs since it reuses that branch's policy matchers; this diff will include #3's changes until #3 merges, which is expected for a stacked branch.

What this closes, and what it doesn't

KNOWN-LIMITS item 2 says outbound capture "is not fully derivable from Claude Code JSONL session transcripts alone" and names MCP-boundary instrumentation as the fix. This is a real step in that direction, not the whole thing, and the docs say so precisely rather than overselling it.

A PostToolUse hook fires after a tool call completes, with both the tool_input and the tool_response the host actually observed. That's captured live, at the moment of execution, by the host — not reconstructed later by parsing a transcript file. Genuine improvement in attestation strength. But it is not wire-level capture: no network proxy, no TLS interception, no independent check that a tool's own response was honest. A tool that lies about its own outcome, or an egress path the matchers don't recognize, isn't caught. True wire-level capture is a larger architectural change, not this PR.

What it does

bin/hook-post-tool-use.js reuses isPushForce, isPushProtected, isPublish, isEgressOther from src/policy/ unchanged — no duplicated matcher logic. On a match, appends an egress-event receipt carrying only digests: paramsDigest, responseDigest, and a best-effort responseOk signal extracted from common success/error shapes. Raw tool_input and raw tool_response are never written to the chain, matching the digest-only convention every other receipt in this repo follows.

No exit-2 path. PostToolUse fires after the call already completed, so there is nothing left to block; the hook always exits 0.

Verification

  • 177 tests passing (172 baseline + 5).
  • The work order's own demo proved no raw content in the receipt for a git push --force payload.
  • I went further in review: independently re-probed with a fabricated exfil URL and a distinctive secret string planted in tool_input/tool_response, then grepped the persisted chain entry directly. Zero hits on both. That's the check that actually matters for a receipt layer's core promise.

Views and docs

  • renderMorningAfter gains an EGRESS EVENTS block (total + per-rule breakdown), same style as the existing POLICY WARNINGS block from gated runs.
  • KNOWN-LIMITS item 2 rewritten to describe the new reality precisely: what's captured now, and what still needs a real network-boundary proxy that isn't in v1.

Real step toward "what actually left the machine" without overclaiming
wire-level capture. A PostToolUse hook fires after the host has observed
both tool_input and tool_response for a completed call. Attested at time
of execution by the host, not reconstructed later from a transcript,
which is a genuine step up over the JSONL-parsing baseline. Explicitly
not a network proxy: no TLS interception, no independent check that a
tool's own response was honest.
- bin/hook-post-tool-use.js: reuses src/policy's isPushForce /
isPushProtected / isPublish / isEgressOther unchanged (no duplicated
matcher logic). On a hit, appends an "egress-event" receipt carrying
only digests: paramsDigest, responseDigest, and a best-effort
responseOk signal. Raw tool_input and raw tool_response are never
written to the chain, matching the digest-only convention every other
receipt in this repo follows. No exit-2 path: PostToolUse cannot block
a call that already completed, so the hook always exits 0.
- views: EGRESS EVENTS block in the morning-after summary, same style
as the existing POLICY WARNINGS block.
- KNOWN-LIMITS 2 rewritten to describe the new reality precisely: what
is captured now, and what still requires a real network-boundary
proxy that is not in v1.
Tests: 177 passing (172 baseline + 5). Independently re-verified beyond
the work order's own demo: a fabricated exfil URL and a distinctive
secret string in tool_input/tool_response both produced zero hits when
grepped against the persisted chain entry.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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

Egress capture: PostToolUse hook for what actually left the machine - #4

Merged
githubscum merged 1 commit into
mainfrom
feat/egress-capture
Jul 22, 2026
Merged

Egress capture: PostToolUse hook for what actually left the machine#4
githubscum merged 1 commit into
mainfrom
feat/egress-capture

Conversation

@githubscum

Copy link
Copy Markdown
Owner

Follows #3 (gated runs). Branched from feat/gated-runs since it reuses that branch's policy matchers; this diff will include #3's changes until #3 merges, which is expected for a stacked branch.

What this closes, and what it doesn't

KNOWN-LIMITS item 2 says outbound capture "is not fully derivable from Claude Code JSONL session transcripts alone" and names MCP-boundary instrumentation as the fix. This is a real step in that direction, not the whole thing, and the docs say so precisely rather than overselling it.

A PostToolUse hook fires after a tool call completes, with both the tool_input and the tool_response the host actually observed. That's captured live, at the moment of execution, by the host — not reconstructed later by parsing a transcript file. Genuine improvement in attestation strength. But it is not wire-level capture: no network proxy, no TLS interception, no independent check that a tool's own response was honest. A tool that lies about its own outcome, or an egress path the matchers don't recognize, isn't caught. True wire-level capture is a larger architectural change, not this PR.

What it does

bin/hook-post-tool-use.js reuses isPushForce, isPushProtected, isPublish, isEgressOther from src/policy/ unchanged — no duplicated matcher logic. On a match, appends an egress-event receipt carrying only digests: paramsDigest, responseDigest, and a best-effort responseOk signal extracted from common success/error shapes. Raw tool_input and raw tool_response are never written to the chain, matching the digest-only convention every other receipt in this repo follows.

No exit-2 path. PostToolUse fires after the call already completed, so there is nothing left to block; the hook always exits 0.

Verification

  • 177 tests passing (172 baseline + 5).
  • The work order's own demo proved no raw content in the receipt for a git push --force payload.
  • I went further in review: independently re-probed with a fabricated exfil URL and a distinctive secret string planted in tool_input/tool_response, then grepped the persisted chain entry directly. Zero hits on both. That's the check that actually matters for a receipt layer's core promise.

Views and docs

  • renderMorningAfter gains an EGRESS EVENTS block (total + per-rule breakdown), same style as the existing POLICY WARNINGS block from gated runs.
  • KNOWN-LIMITS item 2 rewritten to describe the new reality precisely: what's captured now, and what still needs a real network-boundary proxy that isn't in v1.

Real step toward "what actually left the machine" without overclaiming
wire-level capture. A PostToolUse hook fires after the host has observed
both tool_input and tool_response for a completed call. Attested at time
of execution by the host, not reconstructed later from a transcript,
which is a genuine step up over the JSONL-parsing baseline. Explicitly
not a network proxy: no TLS interception, no independent check that a
tool's own response was honest.
- bin/hook-post-tool-use.js: reuses src/policy's isPushForce /
isPushProtected / isPublish / isEgressOther unchanged (no duplicated
matcher logic). On a hit, appends an "egress-event" receipt carrying
only digests: paramsDigest, responseDigest, and a best-effort
responseOk signal. Raw tool_input and raw tool_response are never
written to the chain, matching the digest-only convention every other
receipt in this repo follows. No exit-2 path: PostToolUse cannot block
a call that already completed, so the hook always exits 0.
- views: EGRESS EVENTS block in the morning-after summary, same style
as the existing POLICY WARNINGS block.
- KNOWN-LIMITS 2 rewritten to describe the new reality precisely: what
is captured now, and what still requires a real network-boundary
proxy that is not in v1.
Tests: 177 passing (172 baseline + 5). Independently re-verified beyond
the work order's own demo: a fabricated exfil URL and a distinctive
secret string in tool_input/tool_response both produced zero hits when
grepped against the persisted chain entry.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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

Egress capture: PostToolUse hook for what actually left the machine - #4

Merged
githubscum merged 1 commit into
mainfrom
feat/egress-capture
Jul 22, 2026
Merged

Egress capture: PostToolUse hook for what actually left the machine#4
githubscum merged 1 commit into
mainfrom
feat/egress-capture

Conversation

@githubscum

Copy link
Copy Markdown
Owner

Follows #3 (gated runs). Branched from feat/gated-runs since it reuses that branch's policy matchers; this diff will include #3's changes until #3 merges, which is expected for a stacked branch.

What this closes, and what it doesn't

KNOWN-LIMITS item 2 says outbound capture "is not fully derivable from Claude Code JSONL session transcripts alone" and names MCP-boundary instrumentation as the fix. This is a real step in that direction, not the whole thing, and the docs say so precisely rather than overselling it.

A PostToolUse hook fires after a tool call completes, with both the tool_input and the tool_response the host actually observed. That's captured live, at the moment of execution, by the host — not reconstructed later by parsing a transcript file. Genuine improvement in attestation strength. But it is not wire-level capture: no network proxy, no TLS interception, no independent check that a tool's own response was honest. A tool that lies about its own outcome, or an egress path the matchers don't recognize, isn't caught. True wire-level capture is a larger architectural change, not this PR.

What it does

bin/hook-post-tool-use.js reuses isPushForce, isPushProtected, isPublish, isEgressOther from src/policy/ unchanged — no duplicated matcher logic. On a match, appends an egress-event receipt carrying only digests: paramsDigest, responseDigest, and a best-effort responseOk signal extracted from common success/error shapes. Raw tool_input and raw tool_response are never written to the chain, matching the digest-only convention every other receipt in this repo follows.

No exit-2 path. PostToolUse fires after the call already completed, so there is nothing left to block; the hook always exits 0.

Verification

  • 177 tests passing (172 baseline + 5).
  • The work order's own demo proved no raw content in the receipt for a git push --force payload.
  • I went further in review: independently re-probed with a fabricated exfil URL and a distinctive secret string planted in tool_input/tool_response, then grepped the persisted chain entry directly. Zero hits on both. That's the check that actually matters for a receipt layer's core promise.

Views and docs

  • renderMorningAfter gains an EGRESS EVENTS block (total + per-rule breakdown), same style as the existing POLICY WARNINGS block from gated runs.
  • KNOWN-LIMITS item 2 rewritten to describe the new reality precisely: what's captured now, and what still needs a real network-boundary proxy that isn't in v1.

Real step toward "what actually left the machine" without overclaiming
wire-level capture. A PostToolUse hook fires after the host has observed
both tool_input and tool_response for a completed call. Attested at time
of execution by the host, not reconstructed later from a transcript,
which is a genuine step up over the JSONL-parsing baseline. Explicitly
not a network proxy: no TLS interception, no independent check that a
tool's own response was honest.
- bin/hook-post-tool-use.js: reuses src/policy's isPushForce /
isPushProtected / isPublish / isEgressOther unchanged (no duplicated
matcher logic). On a hit, appends an "egress-event" receipt carrying
only digests: paramsDigest, responseDigest, and a best-effort
responseOk signal. Raw tool_input and raw tool_response are never
written to the chain, matching the digest-only convention every other
receipt in this repo follows. No exit-2 path: PostToolUse cannot block
a call that already completed, so the hook always exits 0.
- views: EGRESS EVENTS block in the morning-after summary, same style
as the existing POLICY WARNINGS block.
- KNOWN-LIMITS 2 rewritten to describe the new reality precisely: what
is captured now, and what still requires a real network-boundary
proxy that is not in v1.
Tests: 177 passing (172 baseline + 5). Independently re-verified beyond
the work order's own demo: a fabricated exfil URL and a distinctive
secret string in tool_input/tool_response both produced zero hits when
grepped against the persisted chain entry.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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