Skip to content

[finding] 「内容恒经 REST 代理(claude[bot])」 — the App IS the writer (performed_via_github_app: claude), but it uses a user-to-server token, so every seat write is bylined to a User account #18168

Description

@os-warren

⚠️ CORRECTED 2026-09-14T13:2xZ — the original filing named the wrong mechanism. It said this container has no Claude-App write channel. That is false: performed_via_github_app.slug reads claude. The real shape is a user-to-server token, which attributes to the acting user. The finding itself stands; its root cause is rewritten below. Correction comment on the thread.

Filed by the domain:spec execution seat (#6017), session_01KB5PFtxuy1x3dcR5gxudx6, on the maintainer's instruction in live PM chat. ⛔ Not claimed, ⛔ not dispatched. Grading and final routing are triage's; domain:skills is set from the measured landing point (the rule's text lives in .claude/skills/pm-dispatch/SKILL.md, governed surface).

The declared rule

.claude/skills/pm-dispatch/SKILL.md, 〈全体座位的不变量〉, verbatim:

  • 用户账号仅三用:assignee、授权批准、维护者亲手;⛔ 席位与 dev 永不以用户账号写内容。
  • 内容恒经 REST 代理(claude[bot]);批准账号永不跑席位、不作席位 claude.ai 的关联用户。

What is actually true — measured, and NOT what this card first said

The rule has two halves and they come apart:

half status reading
「内容恒经 REST 代理」 SATISFIED GET /repos/objectstack-ai/objectstack/issues/comments/5664585381performed_via_github_app.slug: "claude", App id 1236702, owner anthropics, name "Claude"
「署名 claude[bot] NOT satisfied the same comment reads user.login: "os-warren", user.type: "User", id 318460043

⇒ The App writes with a user-to-server token, which GitHub attributes to the acting user and stamps performed_via_github_app. A claude[bot] byline comes from a server-to-server (installation) token instead. So this is ⛔ not "the App is bypassed" — it is "the App acts as a user".

Commit authorship is a third, different answer: git config user.name/user.email (local and --global) = Claude / noreply@anthropic.com, and the last commit on a dispatch branch reads Claude <noreply@anthropic.com>. ⇒ commits are authored by Claude; issue and PR comments carry a seat user as the acting identity. One container, two byline shapes.

Supporting readings

  • GET /user with Authorization: Bearer $GITHUB_TOKEN, without the header, and via MCP get_me → all three return os-warren / type: User.
  • GH_TOKEN and GITHUB_TOKEN are byte-identical (same sha1 7cff5a68335c, length 14, value begins prox…) ⇒ a proxy placeholder, ⛔ not two channels and ⛔ not a real PAT.
  • Root cause of the single identity: /root/.ccr/README.md and platform-readings.md:117 — the CCR egress proxy re-terminates TLS and overrides the caller's Authorization header. ⇒ which account signs a write is ⛔ not a flag a seat can choose.
  • Positive control that the credential is live: headless GET /repos/objectstack-ai/objectstack/issues/6017200.

Why the approval red line is intact

  • os-warren is a fleet seat account (it ran the domain:cli seat through R74 and closed it out), ⛔ not the maintainer's personal account.
  • It is not in GOVERNED_APPROVERS['os-zhuang', 'hotlong'], read from scripts/pm/check-governed-queue-guard.mjs:479 ⇒ 「批准账号永不跑席位」 holds and the governed-merge approval gate is unaffected.
  • ⚠️ author_association on the seat's own comment reads COLLABORATOR, ⛔ not MEMBER. Suggestive of repo-collaborator rather than org-member, but ⛔ not authoritative: every org endpoint and even /repos/{o}/{r}/collaborators/{user}/permission is refused by the Claude proxy ("Access to this GitHub API path is not permitted through this proxy.", documentation_urldocs.anthropic.com), so the authoritative role cannot be read from a seat container at all.

The disposition — ⛔ still not chosen here

Declared-but-unenforced pointed at the protocol's own text (ADR-0049 enforce-or-remove). Two directions, and the corrected mechanism changes what each one costs:

  1. Enforce it — have the App write with a server-to-server (installation) token so the byline really is claude[bot]. ⚠️ Now known to be a token-type change, not a missing channel: the App is already installed and already performing the writes. Cost is narrower than this card first implied, but it also erases which seat wrote what — every seat would become one indistinguishable claude[bot], and this fleet's audit trail currently leans on per-seat bylines (os-bill, os-tesla, os-litant, os-justin, os-sales all appear in live threads).
  2. Correct the text — restate the invariant as what the fleet actually does and what has teeth: writes go through the Claude App, the acting identity must be a fleet seat account, and it must never be a GOVERNED_APPROVERS account nor the maintainer's own.

⚠️ Whichever is chosen, the GOVERNED_APPROVERS separation must survive it — that is the clause with a real gate behind it.

The seat's own read, offered as input and ⛔ not as a ruling: option 2 looks right, because option 1 buys a byline at the cost of per-seat attribution, and the thing the rule is actually protecting (an approver account must never run a seat) is already enforced by a constant with a gate. But this is a governance-text call on a governed surface, so it is the maintainer's.

Recorded deviation (maintainer-authorised)

Shown this measurement in live PM chat, the maintainer ruled: continue on os-warren and record it as a deviation. Writes so signed this round are therefore authorised, ⛔ not an unreported breach.

Dedupe words

performed_via_github_app · claude[bot] · GOVERNED_APPROVERS · user-to-server token · 内容恒经 REST 代理

⛔ This seat did not run a dedupe search (per 「立卡者不查重,只附查重词」).


Generated by Claude Code

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions