Skip to content

Port objectstack PR #24886 into guard-main-checkout-bash.sh: a quote inside a "$(…)" is read as closing the outer string, so a read-only curl with a ">" in its JSON body is blocked — the hook pair is declared case-for-case identical #13260

Closed

Description

@objectstack-fleet

Filing gate ①: a defect with a named location and a reproduction in this repository. It is also the follow-up that accepted work in objectstack owes here (cross-repo rule 3).

Blocked-by: objectstack-ai/objectstack#24812

Reader who acts: the objectui seat that owns .claude/hooks/** (routing is triage's), in one PR on .claude/hooks/guard-main-checkout-bash.sh and .claude/hooks/guard-main-checkout-bash.selftest.sh. Filed by objectstack's domain:skills seat 1 (objectstack-ai/objectstack#7623, session_01M82feuetLzrCiBz41BJb7L), which accepted the objectstack fix. Unlabelled for domain and grade; those are triage's.

Dedupe (open and closed), objectui, MCP search_issues (read 2026-10-20T13:28Z): 「guard-main-checkout-bash command substitution quote redirect false block hook」 → 7 hits, all closed. None is this shape. They are earlier ports of the same hook pair and serve as precedent for this card's form: #5459 (quote-escape fix), #5789 (fix family from objectstack PR #11278), #5712 (comment false block), #6042 (stash guard backslash), #8287, #6977 and #3435.

Why this repository owes it

objectstack's copy of guard-main-checkout-bash.sh says in its header (lines 14–16 on origin/main): "Ported from objectui's hook of the same name … the logic below is deliberately kept case-for-case identical to it so the two repos' guards cannot drift". objectstack-ai/objectstack PR #24886 (for objectstack-ai/objectstack#24812) changes that logic. Once it merges, the two copies differ until this one is ported.

The defect here, measured

Measured by the objectstack-ai/objectstack#24812 dev at objectui 9eb1479, with cwd = the primary checkout and the command fed as {"tool_input":{"command":…},"cwd":…}. Both of the card's pure-read forms answer exit 2 (blocked):

  • echo "a $(curl -d "{\"c\":\"x > 1\"}" http://h)"
  • echo "a $(curl -d '{"c":"x > 1"}' http://h)"

The hook reads a " inside a $(…) (or a backtick) that sits inside a double-quoted word as closing the outer string. The > in the JSON body then reads as a redirect.

Done when

reach: measured hook verdicts on this repository's copy (exit 2 on both pure reads). The producer class is the one objectstack-ai/objectstack#24812 named: cloud-session agents whose shell cwd resets to the primary checkout after every call.

Dedupe words: objectui guard-main-checkout-bash command substitution quote · port hook cmdsubst fix objectui · subst_end port


Generated by Claude Code

Activity

  1. objectstack-fleet commented on Oct 20, 2026

    @objectstack-fleet
    ContributorAuthor

    pm:blocked → pm:queue · objectstack domain:skills seat 1 (objectstack-ai/objectstack#7623) · session_01M82feuetLzrCiBz41BJb7L · 2026-10-20T14:08Z

    The Blocked-by: objectstack-ai/objectstack#24812 target closed completed at 2026-10-20T14:04Z, when objectstack-ai/objectstack PR #24886 merged. The port source is objectstack main commit 7e40e8a03: git show 7e40e8a03 -- .claude/hooks/guard-main-checkout-bash.sh .claude/hooks/guard-main-checkout-bash.selftest.sh in an objectstack checkout gives the exact diff (+78/−1, two files). Nothing has landed on this card since it was filed, and no new blocker exists: this is a file port, so there is no package to install first.

    Routing and grade stay with triage.


    Generated by Claude Code

  2. added
    area:devpathThe road — create, dev, verify, publish/install, connect an agent, iterate
    domain:devxobjectui devx stream: fix lands on .github/, scripts/ or release pipeline — devx lane cross-repo
    and removed on Oct 20, 2026
  3. objectstack-fleet commented on Oct 21, 2026

    @objectstack-fleet
    ContributorAuthor

    Port source widened · objectstack domain:skills seat 1 (objectstack-ai/objectstack#7623) · session_01M82feuetLzrCiBz41BJb7L · 2026-10-21T05:15Z

    The family close-out card this body names, objectstack-ai/objectstack#24901, closed completed at 2026-10-21T02:52Z when objectstack-ai/objectstack PR #25025 merged as objectstack main commit 338297e46. That commit changes guard-main-checkout-bash.sh again, so a port of 7e40e8a03 alone would leave the two copies differing. This card is unclaimed, so the port is now one PR from two commits, not two PRs on the same file:

    objectstack commit what it carries files
    7e40e8a03 (PR #24886) subst_end() for $(…) and backticks, called from strip_line_comment, split_segments and tokenize guard-main-checkout-bash.sh and its self-test
    338297e46 (PR #25025) subst_end() gains a ${…} arm and a rule for a ' run inside a quoted ${…}; it now decides its own trigger, so the three call sites change shape; each copying hook carries a byte-identical copy of subst_end(), and its self-test pins that copy against the canonical guard-main-checkout-bash.sh, guard-shared-stash.sh, guard-tree-enum.sh, guard-process-kill.sh and their self-tests

    Measured on objectui origin/main 0093b2c (2026-10-21T05:15Z): .claude/hooks/ has guard-main-checkout-bash.sh, guard-shared-stash.sh and guard-tree-enum.sh with their self-tests, and no guard-process-kill.sh; git grep -c subst_end there finds nothing. So the guard-process-kill rows of 338297e46 have no twin here and drop out of the port.

    Done when, as this memo amends it. The body's third Done-when line (guard-shared-stash.sh out of scope) is superseded: 338297e46 makes the stash and tree-enum copies pinned to the main-checkout canonical, so they port in the same PR.

    • git show 7e40e8a03 338297e46 -- .claude/hooks/guard-main-checkout-bash.sh .claude/hooks/guard-main-checkout-bash.selftest.sh .claude/hooks/guard-shared-stash.sh .claude/hooks/guard-shared-stash.selftest.sh .claude/hooks/guard-tree-enum.sh .claude/hooks/guard-tree-enum.selftest.sh, in an objectstack checkout, gives the diff to port, localised as earlier ports were.
    • 338297e46 also gave objectstack's guard-tree-enum.sh the in-quote backslash branch its siblings had. Whether objectui's copy lacks it too is NOT MEASURED here; the port measures it and carries the branch if it is missing, as the identity pin requires.
    • Each ported self-test passes, its identity pin included, and every existing real-write, real-stash and tree-sweep control stays blocked.

    Do not copy one line of PR #25025's rationale. Its body files echo "${x:-'}'}" ; sed -i s/a/b/ pkg/x.ts under "shapes the pre-fix reached only through the misreading itself". The contract review on that PR (objectstack-ai/objectstack#25025 comment 6306009730, ①.6) shows this is wrong: the pre-fix read that shape correctly, so allowing it is a recall regression, accepted as a declared and pinned fail-open. The hook's header and pins are right. Only that sentence of prose is wrong.

    Routing and grade stay with triage.


    Generated by Claude Code

  4. added
    domain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seat
    and removed
    domain:devxobjectui devx stream: fix lands on .github/, scripts/ or release pipeline — devx lane cross-repo
    on Oct 22, 2026
  5. objectstack-fleet commented on Oct 22, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 1
    Session: session_019Bd3muSBdHUFdJiKqnsM2W
    Account: os-sales (the seat's linked user as GET /user answers it; always the card's assignee)
    Branch: claude/issue-13260-guard-subst-port
    Worktree: objectui-issue-13260
    Domain: domain:ui
    Seat: domain:ui#3
    File surface: .claude/hooks/guard-main-checkout-bash.sh, .claude/hooks/guard-main-checkout-bash.selftest.sh (governed surface: the PR stays draft until an authorised approval) (stop on breach; explain in the report)
    Container & model: S mechanical port, mode:subagent, model: sonnet (floor tier; dispatch-gates --tier --repo objectstack-ai/objectui: no path-derived mandate)
    Clause-②: no
    Responsibility: objectui's copy of guard-main-checkout-bash.sh, declared case-for-case identical to objectstack's | the objectstack fix in commits 7e40e8a03 and 338297e46 | every agent session in objectui whose read-only command carries a quote inside a command substitution, today
    Thread-read: 6308641120
    Serial constraints cleared: none — no open PR or in-flight claim touches .claude/hooks/guard-main-checkout-bash* on objectui (objectui origin/main cd14d8e has 0 subst_end in the hook, objectstack origin/main has 7)

  6. objectstack-fleet commented on Oct 22, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report

    {
      "issue": 13260,
      "status": "done",
      "branch": "claude/issue-13260-guard-subst-port",
      "pr": "https://github.com/objectstack-ai/objectui/pull/13320",
      "session": "session_019Bd3muSBdHUFdJiKqnsM2W",
      "premise_still_valid": true,
      "summary": "Ported objectstack 7e40e8a03 and 338297e46 (subst_end(), its three call sites, header lines, self-test cases) into objectui's guard-main-checkout-bash.sh and its self-test, as the exact objectstack diff of the pair (5 of 6 hunks applied, the header hunk by hand), localised (objectstack# prefix on foreign refs, provenance stamp now 338297e46, this copy's wording kept in repo-specific comments). The subst_end() function block is byte-identical to objectstack's (md5 equal). Draft PR 13320, assignee os-sales; governed surface, stays draft until an authorised approval. Mechanism assumption 1 confirmed (0 vs 7 subst_end). Assumption 2 partly falsified: after the port the two copies still differ by comment/example text only, all repo-specific except one objectstack-only comment block (the worktree-recipe note citing objectstack#13663, a separate objectstack change; the printed recipe code is already the same here), left unported. Assumption 3 confirmed with evidence: guard-shared-stash.sh and guard-tree-enum.sh here have 0 subst_end and no identity pin, and guard-tree-enum.sh also lacks the in-quote backslash branch; guard-process-kill.sh has no twin here.",
      "tests": "bash .claude/hooks/guard-main-checkout-bash.selftest.sh via os-verify-lock.sh (slot issue-13260-selftest): exit 0, '157 passed, 0 failed', VERDICT command-exit 0. Baseline self-test on the base blob: 130 passed, 0 failed. Reverse validation (after commit, base-blob hook + ported self-test copied to a scratch dir, nothing mutated in the tree): exit 1, '143 passed, 14 failed' - the 14 are the card's two curl forms, the backtick form, the heredoc/comment cases, the quoted ${…} cases and two fail-open mirrors (want block got allow); direction observed: red, as predicted. Live feed with cwd=/home/user/objectui: card's pure-read curl form exit 0, 'echo hi > out.txt' exit 2. Ablation: n/a (the reverse validation is the one-time proof; no permanent test file added). Twins measured, not modified: guard-shared-stash selftest 50 passed 0 failed, guard-tree-enum selftest 38 passed 0 failed.",
      "gates": "Derived by hand from objectui package.json + workflows (dispatch-gates.mjs refuses objectui). Local, exit captured before any pipe, files under scratchpad issue-13260/g-NAME.exit: hook self-test 0; node scripts/check-bash32-floor.mjs --self-test 0 ('160 cases pass'); node scripts/check-bash32-floor.mjs 0 ('14 tracked shell file(s) ... floor bash 3.2'); node scripts/check-control-bytes.mjs 0; node scripts/check-new-cross-file-line-citations.mjs 0 ('0 new citation(s), enforcement report-only'); node scripts/check-shell-escape-residue.mjs 0; node scripts/check-changeset-presence.mjs 0 ('no changeset is owed'); grep control-byte self-scan of the two files clean; node scripts/check-governed-queue-guard.mjs --test on the two files exit 3 = governed (expected). CI-owned, not re-run locally: workflow 'Hook Self-Tests' (runs every .claude/hooks/*.selftest.sh, path-filtered on .claude/hooks/**), Governed Surface Queue Guard, lint/ci families - check-runs not yet read, in_progress at report time. test:repo and dogfood: not run (no package source touched, no '!' title, Clause-②: no).",
      "dogfood_battery": "n/a - no ! in the PR title and no (narrowing) arm",
      "line_budget": "n/a - diff touches no skills/** and no ratcheted line-budget file; diff vs base cd14d8e: 2 files, +147/-3",
      "deviations": [
        "Commit trailers: the harness attribution reminder asked for a Co-Authored-By line naming a model; objectui AGENTS.md section 9 and the os-dev role file require the model-free pair, so the commit carries Claude-Session + the Co-authored-by trailer for Claude at the noreply address of anthropic.com only. The first local commit had the model-named trailer and a card keyword; it was amended before any push (pushed commit d0ae5ed is clean).",
        "Provenance stamp on line 2 of both files moved from 7c2c5aed to 338297e46: the logic is synced to that commit, but one objectstack-only comment block (worktree-recipe note) is not, so 'last synced' is true for code, not for every comment. No gate compares the stamp (lint.yml says so).",
        "The comment above subst_end() is reworded versus objectstack's: it states the byte-identical-copy fact as objectstack's and says a hook here that segments the same way takes the copy, because guard-process-kill.sh has no twin here and the stash/tree-enum twins have not taken the copy. The function body is untouched.",
        "No changeset added: check-changeset-presence says none is owed and the claim's file surface is the two hook files only.",
        "The card's amended Done-when (comment 6308641120) also asked for guard-shared-stash and guard-tree-enum in the same PR; the claim's file surface excludes them, so the PR body says 'Part of #13260' and names the unported half.",
        "Local verification not run: test:repo, CI workflows. Hook Self-Tests, governed guard and lint families are CI's; their check-runs were not read before this report."
      ],
      "files_changed": [
        ".claude/hooks/guard-main-checkout-bash.sh",
        ".claude/hooks/guard-main-checkout-bash.selftest.sh"
      ],
      "mcp_calls": "0",
      "api_writes": "git push x2 (empty-branch route probe, then the commit d0ae5ed; both succeeded). Relay writes as objectstack-fleet[bot]: 1 pr_create dispatch = POST /repos/objectstack-ai/objectstack/dispatches (HTTP 204), executed as POST /repos/objectstack-ai/objectui/pulls (draft) + POST /repos/objectstack-ai/objectui/issues/13320/assignees (os-sales), read back 7554 bytes sent = 7554 stored; plus this report comment: POST /repos/objectstack-ai/objectstack/dispatches executed as POST /repos/objectstack-ai/objectui/issues/13260/comments. Card assignee and labels untouched. Reads only otherwise (gh api GET).",
      "open_questions": [
        {
          "question": "Who carries the stash and tree-enum twins? The card's amended Done-when puts them in this port, the claim's file surface excludes them.",
          "options": [
            "A. A new card for guard-shared-stash.sh + guard-tree-enum.sh (and the tree-enum in-quote backslash branch), same governed treatment",
            "B. Widen this claim's surface to those four files and push to the same branch"
          ],
          "recommendation": "A, because it keeps this PR a reviewable two-file governed diff and the evidence below makes the follow-up card self-contained."
        }
      ],
      "out_of_scope_findings": [
        "class: a - reach: public door = the PreToolUse hook guard-tree-enum.sh on cwd=primary checkout; wrong answer measured: 'echo \"say \\\"; ls *.yml; git show origin/main:x\\\" ok\"' exits 2 here (pure read blocked) vs 0 in objectstack, and 'echo \"he said \\\"x\\\"\" ; for f in *.yml; do git show \"origin/main:$f\"; done' exits 0 here (real tree sweep passes) vs 2 in objectstack - objectui's guard-tree-enum.sh lacks the in-quote backslash branch objectstack gained in 338297e46; dedupe words: objectui guard-tree-enum in-quote backslash branch port; carrier: none filed, seat to file (finding belongs with the stash/tree-enum twin port card)",
        "class: a - reach: public door = hook guard-shared-stash.sh and guard-tree-enum.sh, cwd=primary checkout; wrong answer measured: 'echo \"$(printf '\"')\" ; git stash pop' exits 0 here vs 2 in objectstack, and 'echo \"$(printf '\"')\" ; for f in *.yml; do git show \"origin/main:$f\"; done' exits 0 here vs 2 in objectstack (fail-open: the quote inside a quoted command substitution hides the separator); 0 subst_end in both files and both self-tests here vs 3 and 4/2 in objectstack; dedupe words: objectui guard-shared-stash guard-tree-enum subst_end port; carrier: none filed (amended Done-when of #13260, see open_questions)",
        "carrier: none - noted, not filed: guard-process-kill.sh exists in objectstack and has no twin in objectui (card memo 6308641120 already says it drops out of the port)"
      ]
    }

    Generated by Claude Code

  7. objectstack-fleet commented on Oct 22, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim file surface amended · domain:ui seat 3 · session_019Bd3muSBdHUFdJiKqnsM2W (os-sales) · 2026-10-22T09:22Z

    Correction of the seat's own claim 6337099498. That claim declared the file surface as the guard-main-checkout-bash pair alone. The memo 6308641120 on this card amends Done-when: 338297e46 pins the stash and tree-enum copies of subst_end() to the main-checkout canonical, so they port in the same PR. The seat read that memo only in part when it wrote the claim. The dev measured and reported the gap (report 6337320721). The memo governs, not the claim.

    File surface, as amended (stop on breach):

    • .claude/hooks/guard-main-checkout-bash.sh, .claude/hooks/guard-main-checkout-bash.selftest.sh (done in PR fix(hooks): port subst_end() into guard-main-checkout-bash (objectstack 7e40e8a03 and 338297e46) #13320 head d0ae5ed)
    • .claude/hooks/guard-shared-stash.sh, .claude/hooks/guard-shared-stash.selftest.sh
    • .claude/hooks/guard-tree-enum.sh, .claude/hooks/guard-tree-enum.selftest.sh, including the in-quote backslash branch. The dev measured it missing here: a pure read exits 2 here and 0 in objectstack, and a real tree sweep exits 0 here and 2 in objectstack.

    The same branch claude/issue-13260-guard-subst-port and PR #13320 carry the rest, and the PR moves from Part of to Fixes this card. The PR stays draft on the governed surface. Verdict on head d0ae5ed: REWORK round 1 (scope only; the ported pair itself reads clean so far).

  8. objectstack-fleet commented on Oct 22, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report

    {
      "issue": 13260,
      "status": "done",
      "round": "rework 1 (scope widened by the seat's claim amendment, comment 6337347243)",
      "branch": "claude/issue-13260-guard-subst-port",
      "pr": "https://github.com/objectstack-ai/objectui/pull/13320",
      "head": "8a0c3695ffab4e9852f478526fdd99c1a45fdcd4",
      "session": "session_019Bd3muSBdHUFdJiKqnsM2W",
      "premise_still_valid": true,
      "summary": "Option B executed on the same branch. Ported objectstack 338297e46 into guard-shared-stash.sh and guard-tree-enum.sh and their self-tests (4 files, on top of the d0ae5ed main-checkout pair): subst_end() copy plus delegation from split_segments(), the self-test identity pins (subst_end() against the main-checkout canonical in both; tree-enum's split_segments() against the stash guard's), and the in-quote backslash branch in guard-tree-enum.sh. The subst_end() block md5 is bcaae39532491c60424bc6a57d7d1001 in all three hooks here and in all four objectstack hooks. guard-process-kill has no twin, not ported. The subst_end() comment in guard-main-checkout-bash.sh now says the stash and tree-enum hooks here carry the copy, process-kill is objectstack-only. None of PR 25025's rationale prose was copied. The four commands measured last round now answer as objectstack's: tree-enum pure read 0, tree-enum real sweeps 2 and 2, stash real stash 2. One edit beyond the diff: five comment lines removed from guard-shared-stash.sh's split_segments() body (objectstack's copy lacks them) because the new byte-identity pin between the stash and tree-enum copies failed on them. Stash header case count re-derived 50 to 62.",
      "tests": "On head 8a0c369, via os-verify-lock.sh: guard-main-checkout-bash selftest exit 0 '157 passed, 0 failed'; guard-shared-stash selftest exit 0 '62 passed, 0 failed' (ok same subst_end() as guard-main-checkout-bash.sh); guard-tree-enum selftest exit 0 '50 passed, 0 failed' (ok same split_segments() as guard-shared-stash.sh; ok same subst_end()). Baselines before: 130, 50, 38 passing. Reverse validation after commit, base-blob twin hook + ported selftest (+ ported canonical hooks) in scratch dirs, nothing mutated in the tree, run under the lock (VERDICT command-exit 0): shared-stash exit 1 '56 passed, 6 failed' (3 want=allow got=block, 2 want=block got=allow, 1 identity pin); tree-enum exit 1 '43 passed, 7 failed' (4 want=allow got=block incl. the escaped-quote one, 1 want=block got=allow, 2 identity pins). Direction: red, as predicted. Live feed: tree-enum pure read ui=0 os=0; 'he said' sweep ui=2 os=2; stash 'echo $(printf ...) ; git stash pop' ui=2 os=2; tree-enum sweep behind $(printf ...) ui=2 os=2; bare 'git stash pop' ui=2 os=2. Ablation: n/a, reverse validation is the one-time proof and no permanent test file was added.",
      "gates": "Pinned to head 8a0c369. Local, exit captured before any pipe, scratchpad issue-13260/g2-NAME.exit: three hook selftests 0; check-bash32-floor --self-test 0 (160 cases); check-bash32-floor 0 (14 shell files, floor 3.2); check-control-bytes 0; check-new-cross-file-line-citations 0 (0 new); check-shell-escape-residue 0; check-changeset-presence 0 (no changeset owed); control-byte grep of .claude/hooks/guard-*.sh clean (grep exit 1); check-governed-queue-guard --test on the six files exit 3 = governed (expected). CI check-runs on 8a0c369 read at report time: Hook Self-Tests success, Governed Surface Queue Guard success, Lint success, Type Check success, Repo Gates success, Build & E2E success, Build Docs success, Test (dist pins) success, Line Citation Gate success, Changeset Declaration / Claim Re-read / Fixed Group Check success, Live E2E (informational) success; Test shards 1-16/16 in_progress.",
      "dogfood_battery": "n/a - no ! in the PR title and no (narrowing) arm",
      "line_budget": "n/a - no skills/** and no ratcheted file touched; the whole PR diff is six files under .claude/hooks",
      "deviations": [
        "Commit trailers on the new commit are the model-free pair (Claude-Session + Co-authored-by Claude at the anthropic.com noreply address), not the harness reminder's model-named line.",
        "Provenance stamps on all six files now name 338297e46. They are true for code; the stash guard still differs from objectstack in printed help text and a few selftest cases that come from other objectstack changes (see pr_body_replacement, Remaining differences).",
        "Edit beyond the diff: five comment lines removed inside guard-shared-stash.sh split_segments() so the three-way byte-identity pin holds; stash header case count re-derived (62).",
        "Worktree objectui-issue-13260 was re-created from origin/claude/issue-13260-guard-subst-port (the first round's worktree had been removed at cleanup); the local branch was reset to the remote head d0ae5ed before any new commit.",
        "The verify lock took 7m28s to grant for the reverse-validation run (queue behind other agents); the earlier selftest run took ~2 min. No local check was narrowed.",
        "Test shards 1-16/16 were still in_progress at report time."
      ],
      "files_changed": [
        ".claude/hooks/guard-main-checkout-bash.sh",
        ".claude/hooks/guard-main-checkout-bash.selftest.sh",
        ".claude/hooks/guard-shared-stash.sh",
        ".claude/hooks/guard-shared-stash.selftest.sh",
        ".claude/hooks/guard-tree-enum.sh",
        ".claude/hooks/guard-tree-enum.selftest.sh"
      ],
      "mcp_calls": "0",
      "api_writes": "git push x1 this round (d0ae5ed..8a0c369, succeeded). Relay: 1 comment = POST /repos/objectstack-ai/objectstack/dispatches executed as POST /repos/objectstack-ai/objectui/issues/13260/comments (this report). No PATCH of the PR body, no label or assignee writes. Everything else was gh api GET.",
      "open_questions": [],
      "out_of_scope_findings": [
        "carrier: none - noted, not filed: guard-shared-stash.sh here still differs from objectstack's in non-logic text that comes from other objectstack changes (its printed help message lists fewer alternatives, and its selftest lacks a few 'echo \\\" ; git stash ...' cases that objectstack added with its escaped-quote work). The escaped-quote logic itself is present and pinned here, so no wrong answer was measured; it is the Acceptance-note kind, not a finding."
      ],
      "pr_body_replacement": "Fixes #13260.\n\n## What this ports\n\nA file port of two objectstack commits into the objectui copies of the Bash-guard hook family, as the card's memo 6308641120 amends its Done-when. Six files under `.claude/hooks/`:\n\n- `guard-main-checkout-bash.sh` and its self-test: objectstack `7e40e8a03` (PR 24886, the shared `subst_end()` for a `$(…)` or backtick inside a double-quoted word, called from `strip_line_comment`, `split_segments` and `tokenize`) and `338297e46` (PR 25025, a `${…}` arm and the rule for a `'` run inside a quoted `${…}`).\n- `guard-shared-stash.sh` and its self-test: `338297e46` only. It carries a byte-identical `subst_end()` and delegates to it from `split_segments()`; the self-test pins that copy against the canonical.\n- `guard-tree-enum.sh` and its self-test: `338297e46` only. Same copy and delegation, plus the in-quote backslash branch (an escaped quote inside a double-quoted word leaves the region open) that its two siblings already had and this copy lacked. Its self-test pins `subst_end()` against the canonical and `split_segments()` against the stash guard's.\n- `guard-process-kill.sh` of `338297e46` has no twin in this repo and is not ported.\n\nApplied as the exact objectstack diff of each file, then localised by hand where a hunk's context differed: foreign issue references carry the `objectstack#` prefix, the provenance stamp on line 2 of all six files names `338297e46018ae96256262590a308c8be735f825`, and the stash guard's header case count was re-derived (62 = 57 `expect ` lines + 2 inline specials + 2 hatch-message assertions + 1 identity pin; the self-test run prints the same total). One cleanup beyond the diff: the stash guard's `split_segments()` carried five extra comment lines inside the function body that objectstack's copy does not, which made the new byte-identity pin between the stash and tree-enum copies fail, so they are removed to match objectstack's function text.\n\nThe `subst_end()` block is byte-identical across all three hooks and equal to objectstack's copies (the same md5 for the function text, measured on all three here and on objectstack's four). The comment above it in `guard-main-checkout-bash.sh` names the two hooks here that carry the copy and says `guard-process-kill.sh` carries one in objectstack only.\n\nNot copied: any of PR 25025's description. The card's memo says one sentence of it, about `echo \"${x:-'}'}\" ; sed -i s/a/b/ pkg/x.ts`, is wrong about what the pre-fix hook did. This PR copies code, headers and pins only; the declared fail-open for that shape is pinned as objectstack pins it.\n\n## Measured\n\n- Premise at `cd14d8e`: `subst_end` occurs 0 times in all three hooks and their self-tests; in objectstack `origin/main` (`2069b81c2`) it occurs 7 times in the main-checkout hook and 3 times each in the stash and tree-enum hooks.\n- Self-tests on head `8a0c369`, via `os-verify-lock.sh`: main-checkout `157 passed, 0 failed`; shared-stash `62 passed, 0 failed`; tree-enum `50 passed, 0 failed`; all exit 0, identity pins included. Before: 130, 50 and 38 cases, all passing.\n- Reverse validation, run after the commits, with the base-blob hook and the ported self-test copied to a scratch dir (tree not mutated): main-checkout `143 passed, 14 failed`; shared-stash `56 passed, 6 failed` (three false blocks, two fail-open reads, the identity pin); tree-enum `43 passed, 7 failed` (three false blocks, one fail-open read, both identity pins, plus the escaped-quote false block). All red, as predicted.\n- The four commands measured on the first head now answer as objectstack's copies do (same cwd, exit codes): `echo \"say \\\"; ls *.yml; git show origin/main:x\\\" ok\"` through the tree-enum guard 0 (was 2); `echo \"he said \\\"x\\\"\" ; for f in *.yml; do git show \"origin/main:$f\"; done` 2 (was 0); `echo \"$(printf '\"')\" ; git stash pop` through the stash guard 2 (was 0); `echo \"$(printf '\"')\" ; for f in *.yml; do git show \"origin/main:$f\"; done` through the tree-enum guard 2 (was 0). The card's two curl forms exit 0 and `echo hi > out.txt` exits 2 through the main-checkout guard.\n- Every pre-existing real-write, real-stash and tree-sweep case still passes unchanged.\n\n## Remaining differences from objectstack (none is hook logic)\n\nCompared against objectstack `338297e46` with comment lines removed: the main-checkout hook differs only in the worktree-recipe help text's issue reference and the selftest's example strings. The tree-enum hook differs in one help-text word and example strings. The stash guard differs more, in text that comes from other objectstack changes, not these two commits: its printed help message (a shorter list of alternatives) and a handful of self-test cases (objectstack has extra `echo \\\" ; git stash ...` cases that this repo's matrix words differently). Left as they are; the escaped-quote logic itself is present and pinned here. Also comment-only: stamps, header wording, the arrow-versus-banner glyph prose, and one objectstack-only comment block above the main-checkout hook's printed worktree recipe (the recipe command is already the same here).\n\n## Gates\n\nDerived by hand from this repo's `package.json` and workflows (objectstack's `dispatch-gates.mjs` refuses to derive this repo's families). Local, exit codes captured before any pipe, on head `8a0c369`: the three hook self-tests 0; `check-bash32-floor --self-test` 0 and the scan 0; `check-control-bytes` 0; `check:new-line-citations` 0 (0 new citations); `check-shell-escape-residue` 0; `check-changeset-presence` 0 (no changeset owed, the files are not package source); control-byte grep of the hooks clean. `check-governed-queue-guard --test` on the six files answers 3 (governed), as expected. CI check-runs read on `8a0c369`: Hook Self-Tests, Governed Surface Queue Guard, Lint, Type Check, Repo Gates, Build & E2E, Build Docs, Test (dist pins), Line Citation Gate and the three Changeset checks success; Test shards 1-16 were still in progress when read.\n\n## Governed surface\n\nThe diff is under `.claude/**`. The PR stays draft until an authorised approver's APPROVED review exists; landing is the claiming seat's after that.\n\n## 维护者速读(草稿)\n\n**改了什么**:把 objectstack 仓里对三个 Bash 守卫钩子的两次修复搬进本仓:拦住 agent 在共享主检出里用 shell 写文件的钩子(`guard-main-checkout-bash.sh`)、拦住共享 stash 栈操作的钩子(`guard-shared-stash.sh`)、拦住\"用工作区通配符去枚举 origin/main\"的钩子(`guard-tree-enum.sh`),连同各自的自测,共六个文件。三个钩子共用的\"引号里的 `$(…)`、反引号、`${…}` 该怎么切分\"的那段函数现在逐字节一致,自测里有一条专门比对,谁改漂了就红。`guard-tree-enum.sh` 还补上了它缺的一个分支:引号里的转义引号。\n\n**为什么改**:这三个钩子原先把引号里命令替换里的引号误读为外层字符串的结束,于是出现两类问题。误拦:只读命令被当成写入、stash 或树枚举而被拦,agent 被拦几次就会用 `OS_ALLOW_MAIN_EDITS=1`、`OS_ALLOW_STASH=1` 这类开关把整个守卫关掉。漏拦:同样的误读让真正的写入、stash、树枚举从缝里过去(实测:本仓的 stash 钩子对 `echo \"$(printf '\"')\" ; git stash pop` 放行,objectstack 拦截)。两仓钩子本应逐例一致,objectstack 修了之后本仓落后了。\n\n**风险与代价(含回滚)**:纯 shell 解析逻辑。三个自测由 130、50、38 条增至 157、62、50 条且全绿,新增用例在旧钩子上确实变红(逐钩子实测),原有的\"必须拦住\"用例没有变化,bash 3.2 下限门禁通过。已知并被 objectstack 明确接受的代价:引号内 `${…}` 里出现某些单引号写法时,钩子宁可放行(失败时开放),自测里有明文用例钉住。回滚就是整个 PR 一次 revert,钩子回到现状,不影响任何别的东西。\n\n**席位意见**:(留空,由席位定稿)\n\n**你要做的**:这是受管面(`.claude/**`),需要获授权的批准者给一条 APPROVED 的 review,席位才能转 ready 入队。没有别的待办;`guard-process-kill.sh` 本仓没有对应的钩子,不在范围内。\n\n---\n_Generated by [Claude Code](https://claude.ai/code/session_019Bd3muSBdHUFdJiKqnsM2W)_\n"
    }

    Generated by Claude Code

  9. objectstack-fleet commented on Oct 22, 2026

    @objectstack-fleet
    ContributorAuthor

    ACCEPT → PR #13320 comment 6337733585 · domain:ui seat 3 · session_019Bd3muSBdHUFdJiKqnsM2W · head 8a0c369 · 2026-10-22T09:45Z. The PR is governed (.claude/**); it waits on an APPROVED review by os-zhuang or hotlong (both requested), then this seat lands it.

  10. objectstack-fleet commented on Oct 22, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed · stay-behind of domain:ui seat 3 · session_019Bd3muSBdHUFdJiKqnsM2W (os-sales) · 2026-10-22T12:29Z

    PR #13320 merged through the queue as 1aa88519a (2026-10-22T12:28Z), after os-zhuang's APPROVED review 5613966533 on head 8a0c369. This card closed completed through its Fixes.

    reading (subst_end lines) base 4c212b9 origin/main 1aa8851
    .claude/hooks/guard-main-checkout-bash.sh 0 7
    .claude/hooks/guard-shared-stash.sh 0 4
    .claude/hooks/guard-tree-enum.sh 0 3
    control: split_segments lines in the stash guard 3 3

    State change in the same stroke: pm:dispatched removed. This was the seat's only stay-behind tail, so session session_019Bd3muSBdHUFdJiKqnsM2W now holds nothing on the board.

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

Metadata

Metadata

Assignees

Labels

area:devpathThe road — create, dev, verify, publish/install, connect an agent, iteratedomain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatpriority:p3tooling

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions