Context
Successor to #16535, carved out while implementing PR #16659.
#16535's AC3 requires the confidentiality check on "the agent PR-body lint, issue bodies, issue comments, and Discussion bodies/comments — every surface the open internet can search." PR #16659 delivers all of those except the PR-body lint, because the MCP dispatch guard covers everything that goes through PUBLIC_GITHUB_WRITE_TOOLS and the Actions lint is the one path that does not.
Carving it out rather than rushing it: the Actions path has two design constraints that invert the local design, and neither is a wiring detail.
The Problem
.github/workflows/agent-pr-body-lint.yml and agent-pr-review-body-lint.yml are the guards on the two body surfaces authored outside MCP dispatch. Reusing confidentialTokenScanner there looks like a five-line step and is not.
1. The Actions log is public — the fix can commit the harm
The local design deliberately returns the matched token and its offset so the author can scrub. That is correct for a local tool response and catastrophic in Actions: workflow logs on a public repository are world-readable, so a lint that prints the match publishes the client name to the exact audience #16535 exists to protect against. It would be the harm delivered by the guard.
GitHub masks registered secret values in logs, but that masking is a backstop against accidental echo, not a design to lean on — and it does not cover derived output like an offset-annotated excerpt or a diff context line.
So this path needs the inverse output contract: report that a token matched and where, never which. matches[].token must not cross into Actions output, which the current scanner returns unconditionally.
2. Fork PRs do not receive secrets — coverage inverts against trust
The denylist cannot be committed (#16535 OQ1), so in Actions it must come from a repository secret. pull_request runs from forks receive no secrets. The check would therefore be unchecked precisely on contributions from outside the org — the least-trusted authors — and enforced only on branches from within it.
That is not a reason to skip it: an internal agent authoring a PR body is the actual recurrence population, all three recorded instances being ours. But a guard whose coverage is strongest where trust is highest and absent where it is lowest must say so, or it will be read as covering a surface it does not.
pull_request_target would supply secrets and is the wrong answer — it runs trusted-context workflows against untrusted head code, which trades a confidentiality gap for a code-execution one.
The Architectural Reality
.github/workflows/agent-pr-body-lint.yml, .github/workflows/agent-pr-review-body-lint.yml — the two Actions surfaces.
ai/services/shared/confidentiality/confidentialTokenScanner.mjs (PR #16659) — reusable, but its matches[].token is designed for a local caller and must be projected away here.
SCAN_OUTCOME.unchecked — already a first-class state, so "secret absent on a fork run" has a correct representation rather than needing a new one.
- The MCP dispatch guard in
toolService.mjs — covers issue/discussion/comment/review surfaces; a PR body created via gh pr create reaches none of it.
Contract Ledger
| Target Surface |
Source of Authority |
Proposed Behavior |
Fallback |
Docs |
Evidence |
| Actions lint output |
this ticket |
reports outcome + offset count; never the matched token |
unchecked when the secret is absent |
workflow comment stating why the token is withheld here but returned locally |
a spec asserting no denylist value appears in the emitted output for a body that matches |
| denylist source in Actions |
repo secret |
supplied to non-fork runs |
unchecked, stated in the check summary |
note that fork runs are structurally unscanned |
a fork-shaped run reports unchecked, not clean |
| scanner projection |
confidentialTokenScanner |
a caller-selectable projection that omits matches[].token |
— |
JSDoc naming the public-log hazard |
asserting the projected shape has no token field at all, not merely an empty one |
Acceptance Criteria
Out of Scope
- The MCP dispatch guard — delivered in PR #16659. This ticket is the one surface it structurally cannot reach.
pull_request_target as a route to secrets on fork runs. Named as rejected above; adopting it needs its own security review, not a line in this ticket.
- Historical remediation of existing occurrences — operator-directed sweep, per #16535.
resources/content/ mirrors — a separate public surface reached by the sync pipeline; believed covered transitively but verified by nobody, and carried forward from #16535 in that wording rather than upgraded.
Avoided Traps
- Reusing the local output contract. Returning the matched token is right locally and publishes it here. The same function, two callers, opposite disclosure rules.
- Reading a green lint as coverage. Without the secret it is
unchecked, and on forks it is structurally unchecked — a check that is green because it never ran.
pull_request_target for secret access. Trades a confidentiality gap for arbitrary-code execution in a trusted context.
Decision Record impact
none — extends an established boundary discipline to a surface outside MCP dispatch.
Related
#16535 (parent — MCP-dispatch surfaces delivered by PR #16659) · PR #16659 · #16520.
Live latest-open sweep: checked the latest 20 open issues at 2026-08-08T06:23:20Z; no equivalent found. A2A in-flight claim sweep: 8 most recent messages scanned, no competing [lane-claim] on this scope.
Origin Session ID: 9ced67a1-8f21-4da2-a1bf-a2a968c47ed2
Retrieval Hint: query_raw_memories("PR-body lint confidential token public Actions log leak, fork PRs receive no secrets, unchecked on fork run")
Retrieval Hint: the discriminating read is that the scanner's matches[].token is correct for a local author response and is the leak itself in a public workflow log.
Context
Successor to #16535, carved out while implementing PR #16659.
#16535's AC3 requires the confidentiality check on "the agent PR-body lint, issue bodies, issue comments, and Discussion bodies/comments — every surface the open internet can search." PR #16659 delivers all of those except the PR-body lint, because the MCP dispatch guard covers everything that goes through
PUBLIC_GITHUB_WRITE_TOOLSand the Actions lint is the one path that does not.Carving it out rather than rushing it: the Actions path has two design constraints that invert the local design, and neither is a wiring detail.
The Problem
.github/workflows/agent-pr-body-lint.ymlandagent-pr-review-body-lint.ymlare the guards on the two body surfaces authored outside MCP dispatch. ReusingconfidentialTokenScannerthere looks like a five-line step and is not.1. The Actions log is public — the fix can commit the harm
The local design deliberately returns the matched token and its offset so the author can scrub. That is correct for a local tool response and catastrophic in Actions: workflow logs on a public repository are world-readable, so a lint that prints the match publishes the client name to the exact audience #16535 exists to protect against. It would be the harm delivered by the guard.
GitHub masks registered secret values in logs, but that masking is a backstop against accidental echo, not a design to lean on — and it does not cover derived output like an offset-annotated excerpt or a diff context line.
So this path needs the inverse output contract: report that a token matched and where, never which.
matches[].tokenmust not cross into Actions output, which the current scanner returns unconditionally.2. Fork PRs do not receive secrets — coverage inverts against trust
The denylist cannot be committed (#16535 OQ1), so in Actions it must come from a repository secret.
pull_requestruns from forks receive no secrets. The check would therefore beuncheckedprecisely on contributions from outside the org — the least-trusted authors — and enforced only on branches from within it.That is not a reason to skip it: an internal agent authoring a PR body is the actual recurrence population, all three recorded instances being ours. But a guard whose coverage is strongest where trust is highest and absent where it is lowest must say so, or it will be read as covering a surface it does not.
pull_request_targetwould supply secrets and is the wrong answer — it runs trusted-context workflows against untrusted head code, which trades a confidentiality gap for a code-execution one.The Architectural Reality
.github/workflows/agent-pr-body-lint.yml,.github/workflows/agent-pr-review-body-lint.yml— the two Actions surfaces.ai/services/shared/confidentiality/confidentialTokenScanner.mjs(PR #16659) — reusable, but itsmatches[].tokenis designed for a local caller and must be projected away here.SCAN_OUTCOME.unchecked— already a first-class state, so "secret absent on a fork run" has a correct representation rather than needing a new one.toolService.mjs— covers issue/discussion/comment/review surfaces; a PR body created viagh pr createreaches none of it.Contract Ledger
uncheckedwhen the secret is absentunchecked, stated in the check summaryunchecked, notcleanconfidentialTokenScannermatches[].tokenAcceptance Criteria
uncheckedin the check summary, never a pass, and names that enforcement did not run.agent-pr-review-body-lint.yml, since a review body is equally googleable.Out of Scope
pull_request_targetas a route to secrets on fork runs. Named as rejected above; adopting it needs its own security review, not a line in this ticket.resources/content/mirrors — a separate public surface reached by the sync pipeline; believed covered transitively but verified by nobody, and carried forward from #16535 in that wording rather than upgraded.Avoided Traps
unchecked, and on forks it is structurallyunchecked— a check that is green because it never ran.pull_request_targetfor secret access. Trades a confidentiality gap for arbitrary-code execution in a trusted context.Decision Record impact
none— extends an established boundary discipline to a surface outside MCP dispatch.Related
#16535 (parent — MCP-dispatch surfaces delivered by PR #16659) · PR #16659 · #16520.
Live latest-open sweep: checked the latest 20 open issues at 2026-08-08T06:23:20Z; no equivalent found. A2A in-flight claim sweep: 8 most recent messages scanned, no competing
[lane-claim]on this scope.Origin Session ID: 9ced67a1-8f21-4da2-a1bf-a2a968c47ed2
Retrieval Hint:
query_raw_memories("PR-body lint confidential token public Actions log leak, fork PRs receive no secrets, unchecked on fork run")Retrieval Hint: the discriminating read is that the scanner's
matches[].tokenis correct for a local author response and is the leak itself in a public workflow log.