Skip to content

PM write-identity locks 1–4: deny MCP GitHub content writes in settings, REST-only writes for seats and devs with a declared write budget, batch default 2, user accounts limited to assignee and approval #18068

Description

@claude

Maintainer direct dispatch — the maintainer (pm@objectstack.ai) in the skills seat's chat session at 2026-09-13T16:14Z, verbatim: 「机制层的五道锁 现在就派发处理」, after the seat's analysis of the 2026-09-13 account suspension (os-musk): the root cause is the WRITE IDENTITY — content written through the MCP GitHub tools is authored by the claude.ai account's linked GitHub user (measured: os-musk's 12 hidden cards; #18045 and objectui#9404 authored os-project-manager; PR #18051 authored os-project-manager; the services seat's PRs #18035/#18039/#18043 authored hotlong), while content written through the REST proxy is authored by the App (claude[bot], measured on every REST comment of the day). Concurrency amplified the shape; identity is what a suspension acts on. Filed and claimed by the skills seat (session session_01DAcomhvR9kKizeYgg89Vo8) under the direct-dispatch channel; this card carries locks 1–4 (governed rules layer, one approval); lock 5 is its own card.

Dedupe keywords: write identity, claude[bot], MCP write tools, permissions.deny, batch default 2, REST proxy.

Lock 1 — MCP GitHub content writes unreachable (.claude/settings.json)

Add a permissions.deny list naming every MCP GitHub tool that CREATES content or history: mcp__github__issue_write, mcp__github__create_pull_request, mcp__github__add_issue_comment, mcp__github__add_comment_to_pending_review, mcp__github__add_reply_to_pull_request_comment, mcp__github__pull_request_review_write, mcp__github__push_files, mcp__github__create_or_update_file, mcp__github__delete_file, mcp__github__create_branch, mcp__github__sub_issue_write, mcp__github__merge_pull_request, mcp__github__create_repository, mcp__github__fork_repository. KEEP allowed (state, not content; some have no REST equivalent through the proxy): mcp__github__update_pull_request (REST cannot un-draft), mcp__github__enable_pr_auto_merge / disable_pr_auto_merge, mcp__github__actions_run_trigger, mcp__github__resolve_review_thread, every read tool. The dev measures whether Claude Code honours a repo-level permissions.deny for MCP tool names in this environment (read the Claude Code settings docs; state the reading) and records it in references/platform-readings.md if a row is owed (references tier).

Lock 2 — devs make no board API writes beyond three (.claude/agents/os-dev.md)

Rule: every GitHub write a dev makes goes through the REST proxy (curl with the environment GITHUB_TOKEN, Content-Type: application/json); the whole budget is git push + one POST /pulls (draft) + POST /issues/{n}/labels (skip-changeset) + one os-dev-report comment; ⛔ no MCP GitHub write tool, ⛔ no board enumeration (card and thread via the payload channel or repo-scoped single-card reads). The report gains a field api_writes (count + endpoints) that the seat verifies. Replace the :51–:58 dedupe-channel lines (already stale after ruling ③: the filer attaches keywords) so the file stays at 403/403.

Lock 3 — batch default 2 (SKILL.md :60; core-rules.md mirror if its line states a default)

「默认 3」 → 「默认 2」; the maintainer ceiling 5 unchanged; the seat post protocol line 「accelerator armed while devs are in flight」 unchanged. Maintainer's own words on the services seat today: 「当前任务处理完,后续并发降到2」.

Lock 4 — account roles (SKILL.md 〈全体座位的不变量〉 or 〈座位贴协议〉, one line; core-rules.md mirror)

User accounts do three things only — assignee (a label-state, never content), authorized approval, the maintainer's own hand; ⛔ a seat or dev never writes content as a user account; ⛔ an approver account (os-zhuang, hotlong) never runs a seat and is never the linked user of a seat's claude.ai account. Content identity = the session ID in the body (the Claim: Session: line, Reviewed-by:, the filing header), never the author field.

Budgets and gates

Every file is at its ceiling (SKILL.md 812/812, widest row 342 B; core-rules.md 151/151; os-dev.md 403/403): net 0 lines per file, density payments in the same file named per commit, 120-byte cap, frame block :733–:754 md5 3327d02c56f8a0eca88569dad2270f32 unchanged; check:pm-skill-ratchet, check:skill-frame-sync, check:pm-governed-prose, check:pm-skill-id-lint green. One commit per lock, each quoting the ruling. .claude/settings.json is a governed path (rules layer) — the PR stays draft until an authorized approval; the skills seat lands it under ruling C.

Executable criterion

git show origin/main:.claude/settings.json | grep -c mcp__github__issue_write 0 → 1 (inside permissions.deny); git grep -c '默认 \2`' .claude/skills/pm-dispatch/SKILL.md0 → 1 and 「默认3」 1 → 0; git grep -c 'api_writes' .claude/agents/os-dev.md` 0 → 1; a line naming 「approver」 / 「批准账号」 with 「never runs a seat」 semantics 0 → 1 in SKILL.md; lit controls on neighbouring phrases unchanged.


Generated by Claude Code

Activity

  1. claude commented on Sep 13, 2026

    @claude
    ContributorAuthor

    Claim: PM loop round 1
    Session: session_01DAcomhvR9kKizeYgg89Vo8 (GitHub os-project-manager, skills seat), claimed at 2026-09-13T16:18Z
    Branch: claude/issue-18068-write-identity-locks
    Worktree: objectstack-issue-18068
    Domain: domain:skills (GOVERNED rules layer: .claude/settings.json, SKILL.md, core-rules.md, os-dev.md ⇒ four-piece at the end, then ruling C landing by this seat once an authorized approval exists). Channel: maintainer direct dispatch — pm@objectstack.ai in this seat's chat session at 2026-09-13T16:14Z: 「机制层的五道锁 现在就派发处理」; the pair (pm:dispatched + assignee) was written at creation through the REST proxy and read back.
    File surface: .claude/settings.json (a permissions.deny list of the MCP GitHub content-writing tools; the allow list untouched), .claude/skills/pm-dispatch/SKILL.md (:60 batch default 3 → 2; one account-roles line; one REST-proxy write line), .claude/skills/pm-dispatch/references/core-rules.md (mirrors only where a mirrored line exists), .claude/agents/os-dev.md (write channel + api_writes budget, replacing the stale :51–:58 dedupe-channel lines). All at ceiling ⇒ net 0 lines per file, density payments in the same file, 120-byte cap, frame md5 unchanged. references/platform-readings.md only for the measured deny-list reading (references tier, declared).
    Container & model: M, mode:subagent, model: claude-fable-5-1 — dispatch-gates --tier mandates fable for SKILL.md and os-dev.md, and this card adds rules (not a one-line mechanical edit), so no exit applies; seat review at the contract-review tier.
    Clause-②: no
    Thread-read: none — the card is new; the body is the whole thread at this claim
    Serial constraints cleared at 2026-09-13T16:18Z: SKILL.md :106/:113, os-dev.md :50 and lanes/ui.md :25 are on PR #18051's face (draft, awaiting approval) — this card touches DIFFERENT lines of SKILL.md and os-dev.md; the dev branches from origin/main and, if PR #18051 lands first, merges origin/main before the PR; whichever lands second resolves; no other branch names these files; verify lock free; rate ≥14.7k/15k; origin/main at a2497861. Devs in flight after this one: 3 (#17959, #18068, #18069) — the maintainer's direct order 「现在就派发」 is the authority over this seat's own cap of 2; the maintainer ceiling 5 holds.


    Generated by Claude Code

  2. claude commented on Sep 13, 2026

    @claude
    ContributorAuthor

    os-dev-report

    {
      "issue": 18068,
      "status": "done",
      "branch": "claude/issue-18068-write-identity-locks",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/18072",
      "premise_still_valid": true,
      "summary": "Locks 1–4 landed as a draft PR, one commit per lock, each quoting its ruling (「机制层的五道锁 现在就派发处理」; lock 3 「当前任务处理完,后续并发降到2」; lock 2's payment justified by ruling ③ 「立卡者不查重,只在卡面附 3–5 个查重词」). Lock 1: `.claude/settings.json` gains `permissions.deny` with the card's 14 `mcp__github__*` content-writing tools, allow list and hooks untouched. Lock 2: os-dev.md :51–:58 replaced by the REST-proxy-only write rule, the four-write budget, no MCP write / no board enumeration, findings-to-report, and `api_writes` in the report (403/403). Lock 3: SKILL.md :60 「默认 `3`」 → 「默认 `2`」, no core-rules mirror (:11 states no default). Lock 4: two lines in SKILL.md 〈全体座位的不变量〉 plus the identity clause on the 〈认领〉 shared-identity line, one mirror line in core-rules, all paid by density in the same section (812/812, 151/151, frame md5 unchanged). The P2 docs reading is recorded as two 文档载明未实测 lines in platform-readings (451 → 453 under the standing exception, THIRTEENTH ruledRaises record). Assignee was set by the dispatch; no assignee written by me.",
      "tests": "Derived union at 88e0610f: `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` = 42 families, each run in the foreground with the exit code captured before any pipe; all 42 exit 0; `--ran` verdict: 「Run reconciliation — 42 derived, 42 run, 0 NOT-MEASURED, 0 UNRUN」. One first answer was exit 3 (`check:doc-formula-expressions`, PREREQUISITE NOT MET: lint/formula unbuilt) — built via `bash scripts/pm/os-verify-lock.sh -c 'NODE_OPTIONS=--max-old-space-size=4096 pnpm exec turbo run build --filter=@objectstack/formula --filter=@objectstack/lint --concurrency=2'` (VERDICT command-exit 0, held 187s, waited 0s) and re-run: exit 0. Rule ⑤ for the ratchet-script edit: `--self-test` inside `check:pm-skill-ratchet` (exit 0; cross-file move verdict unchanged at +11 against a net source decrease of 20), `check:ratchet-remedy-authority` 0, `node scripts/check-published-list-mirrors.mjs` 0, `node scripts/check-skills-token-ratchet.mjs` 0. After `git merge origin/main` (e248c4d6; no incoming commit touched these files) at head 7e1aecad: `check:pm-skill-ratchet`, `check:pm-skill-id-lint`, `check:skill-frame-sync`, `check:pm-governed-prose`, `check:nul-bytes`, `check:agent-model-declared` all exit 0. `node -e JSON.parse` on settings.json passes. Every added prose line ≤ 120 B; SKILL.md over-120 baseline 23 → 23 (all table rows); control-character scan empty. No build/test suites owed: no package touched. Ablation: none applicable (no gate logic changed; the ratchet edit is a ceiling row + record, proven by the gate's own green verdict at both heads).",
      "files_changed": ".claude/settings.json (+16); .claude/agents/os-dev.md (11/11, 403/403); .claude/skills/pm-dispatch/SKILL.md (6/6, 812/812, widest row 342 B); .claude/skills/pm-dispatch/references/core-rules.md (3/3, 151/151); .claude/skills/pm-dispatch/references/platform-readings.md (+2, 453/453); scripts/pm/check-skill-line-ratchet.mjs (+33/-1: ceiling 451 → 453, comment, ruledRaises record); plus the merge commit of origin/main e248c4d6",
      "premise_checks": "P1 re-measured at BASE 6d647858: settings.json had `permissions.allow` (47 entries) and no `deny`; SKILL.md :60 read 「默认 `3`」 (1 hit, 「默认 `2`」 0); core-rules :11 states the parallelism rule with no default (no mirror needed); os-dev.md :51–:58 were the dedupe-channel lines (probe-then-choose, 403 ⇒ MCP search_issues, control words, no wide scan, payload tier ×2, MCP-for-writes). P2 (Claude Code docs, code.claude.com/docs/en/permissions and /settings): 「Rules are evaluated in order: deny, then ask, then allow」; 「If a tool is denied at any level, no other level can allow it」; 「deny rules from any scope are evaluated before allow rules」; MCP per-tool rule spelling `mcp__server__tool` (parenthesised `mcp__` rules are skipped, mine have none); 「Shared project settings (.claude/settings.json): read, because the file is part of the clone」 in cloud sessions; 「deny and ask rules apply right away」 without workspace trust; a bare-name deny removes the tool from Claude's context ⇒ lock 1 is enforced, not merely declared. P3 (open PR file lists read once via REST): no open PR touches settings.json or core-rules.md; PR #18051 (open draft, head ac73575c) edits os-dev.md :50 and SKILL.md :106/:113 — different lines; origin/main moved 3 commits since BASE, none on these files. Executable criterion at 7e1aecad: `grep -c mcp__github__issue_write .claude/settings.json` 0 → 1 inside deny; SKILL.md 「默认 `2`」 0 → 1, 「默认 `3`」 1 → 0; os-dev.md `api_writes` 0 → 2, `search_issues` 1 → 0; SKILL.md 「批准账号」/「永不跑席位」 0 → 1; core-rules 「批准账号」 0 → 1; controls 「每个方案必须沿四条固定评估轴分析」 1 → 1, 「一座位一车道双射」 1 → 1 (both files), os-dev.md `mcp_calls` 2 → 2. Frame :733–:754 md5 3327d02c56f8a0eca88569dad2270f32 before and after.",
      "deviations": "(1) Lock 4 is two SKILL.md lines, not one — the card's four clauses (three uses; never write as a user; approver never seats nor is the linked user; identity = session ID) do not fit one 120-byte line; both paid inside 〈全体座位的不变量〉, and the identity clause replaced the 〈认领〉 shared-identity line in place. (2) Lock 2 edits four lines outside :51–:58, each net 0: the control-word rule folded into rule 6's premise line; resource rule 6's tail now routes late results to the report; the label-write fallback (:302) no longer prescribes an MCP `issue_write`; the report template's `out_of_scope_findings` example no longer shows a dev-filed card number. (3) The card's budget contains no PR-body `PATCH`; the rule states that explicitly — I did not PATCH this PR's body. (4) platform-readings +2 with a ratchet-script edit (ceiling + ruledRaises record) rather than leaving the reading unrecorded — the card's 「if a row is owed」 read as owed; the file has no tables, so the rows went under 〈读数陷阱〉 beside the MCP invalid-session pair. (5) `git merge origin/main` added one merge commit on top of the five lock commits.",
      "open_questions": [
        { "question": "Should a dev be allowed one `PATCH /pulls/{n}` to refresh its own PR body with late gate results?", "options": ["A — keep the card's four-write budget as landed (late results go to the report comment; the seat reads the report)", "B — add a fifth REST write for the dev's own PR body, authored `claude[bot]` like the rest"], "recommendation": "A, because the report comment is already the authoritative deliverable and the seat verifies `api_writes` against a fixed count; B is one clause on the budget line if the seat prefers it." }
      ],
      "out_of_scope_findings": [
        "noted, not filed (承接者: the skills seat reviewing PR #18072): `mcp__github__update_pull_request_branch` writes merge commits and `request_copilot_review` writes a review request; neither is on the card's 14-tool list, so both stay allowed",
        "noted, not filed (承接者: the skills seat): SKILL.md :778 still says 「只列三类立卡与 noted, not filed」 while lock 2 makes the dev list findings to file and the seat file them; a one-word change on that line if wanted"
      ],
      "mcp_calls": "0 — no MCP GitHub tool was called; every read went through git or repo-scoped REST",
      "api_writes": "3 REST writes — POST /repos/objectstack-ai/objectstack/pulls (PR #18072, draft) · POST /repos/objectstack-ai/objectstack/issues/18072/labels (skip-changeset) · POST /repos/objectstack-ai/objectstack/issues/18068/comments (this report); plus 3 git pushes to claude/issue-18068-write-identity-locks (empty probe, five lock commits, merge of origin/main); reads: 7 repo-scoped GET calls (issue, claim comment, open PR list, 2 PR file lists, PR #18051 state, one comment page)"
    }

    Generated by Claude Code

  3. claude commented on Sep 13, 2026

    @claude
    ContributorAuthor

    Follow-up on PR #18072 (write-identity locks 1–4): the report template line "out_of_scope_findings" in .claude/agents/os-dev.md ran 141 bytes on head 7e1aeca against a base (e248c4d) with zero lines over 120 — a fence-exempt regression of the file's convention. Shortened to 106 bytes at the same line count and pushed as a new commit (no amend, no force-push): head a4dfd9d0b36461e4753d4ccf0a916fd6c603ac19. Measured there: awk 'length($0)>120' .claude/agents/os-dev.md prints nothing (1 → 0), 403/403, pnpm check:pm-skill-ratchet exit 0, pnpm check:nul-bytes exit 0. api_writes for the run is now 4 REST writes (this comment is the fourth); mcp_calls stays 0.


    Generated by Claude Code

  4. claude commented on Sep 13, 2026

    @claude
    ContributorAuthor

    ACCEPT — skills seat (session session_01DAcomhvR9kKizeYgg89Vo8), 2026-09-13T17:23Z. The os-dev-report (5654712923) and the follow-up (5654837902) are read in full; PR #18072 head a4dfd9d0 reviewed in-seat at claude-fable-5-1: ## Contract review PASS with 速读终稿 on the PR. Platform-readings +2 verified per item under the standing exception: candidates 2 / landed 2 / already present 0 / refused 0, one fact per line, marked 文档载明未实测, no reordering — the ruledRaises record (2026-09-13, delta 2) stands. The dev's open question answered on the record: A (four-write budget, no PR-body PATCH). GOVERNED rules layer ⇒ four-piece: needs-user-decision on the PR, reviews requested from os-zhuang and hotlong, draft kept; ruling C landing by this seat once an authorized approval exists. Fixes #18068 closes this card on landing; the objectui settings.json copy and its AGENTS.md §9 line are this seat's next filing batch.


    Generated by Claude Code

  5. claude commented on Sep 13, 2026

    @claude
    ContributorAuthor

    Follow-up on PR #18072 after the merge-queue ejection (MERGE_CONFLICT once PR #18051 landed): merged origin/main 137eb00 into the branch as a merge commit (no rebase, no amend, no force-push) — new head 3a3f5e0db2aff23ced9e6326b7a6ac9c1804567d. One file conflicted, .claude/agents/os-dev.md (:50–:57, the lines adjacent to lock 2); resolved by keeping both sides' intent: #18051's rewritten :50 line stays byte-identical to origin/main, and the five lock-2 lines that follow it stay byte-identical to the reviewed head a4dfd9d; #18051's superseded 「通道先探后选」 line does not return. .claude/skills/pm-dispatch/SKILL.md merged cleanly (:106/:113 equal origin/main; the lock lines untouched). Ceilings at the new head: os-dev.md 403/403 with zero lines over 120 bytes; SKILL.md 812/812, widest row 342 B, frame block :733–:754 md5 3327d02c56f8a0eca88569dad2270f32; core-rules.md 151/151. Gates at 3a3f5e0: pnpm check:pm-skill-ratchet exit 0, pnpm check:skill-frame-sync exit 0, pnpm check:pm-governed-prose exit 0, node -e 'JSON.parse(...)' on .claude/settings.json exit 0. api_writes for the run is now 5 REST writes (this comment is the fifth); mcp_calls stays 0.


    Generated by Claude Code

  6. claude commented on Sep 13, 2026

    @claude
    ContributorAuthor

    Landed — skills seat (session session_01DAcomhvR9kKizeYgg89Vo8), 2026-09-13T23:53Z. PR #18072 merged by the queue as 7ef05f997 (single parent c54d8d67b); two readings at 2026-09-13T23:52Z: the queue ref gh-readonly-queue/main/pr-18072-* is gone, and git log origin/main carries (#18072) with (#18051) as the lit control. Ruling C landing after one queue ejection: approval os-zhuang 5192548807 on a4dfd9d0 (not dismissed, stands under ruling C through the merge-commit head 3a3f5e0d); review of record 5654849272, re-issued on the merge-commit head as 5656815610; step records 5656676373 (first arming) and 5656817728 (re-landing after the 2026-09-13T23:00Z MERGE_CONFLICT ejection against PR #18051's landing; the dev's origin/main merge is follow-up 5656804868). Locks 1–4 are now on origin/main: .claude/settings.json denies the 14 MCP GitHub content-writing tools (applies to sessions that load the landed settings); os-dev.md writes go REST-only under a declared budget with the api_writes report field; SKILL.md batch defaults to 2 (maintainer ceiling 5) and the account-roles lines name assignee / authorized approval / the maintainer's own hand as the only user-identity writes; core-rules mirrors; platform-readings 453/453 under the standing exception with the ruledRaises record in check-skill-line-ratchet.mjs. Lock 5 (H64) landed earlier as PR #18073 → 9d288d40b. Residue (pm:dispatched, assignee) stripped in this pass and read back. The objectui half (settings deny list + AGENTS.md §9) is objectui#9418, Blocked-by: this PR — now unblocked for its own lane.


    Generated by Claude Code

  7. added a commit that references this issue on Sep 17, 2026
    7ef05f9
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions