Skip to content

guard hooks: guard-shared-stash.sh still reads a nested quote inside "$(…)" as closing the outer string, and guard-main-checkout-bash.sh does the same for "${x:-"…"}" — the #24812 family close-out #24901

Closed

Description

@objectstack-fleet

Filing gate ①: a defect with a named location and a reproduction. This is the family close-out card for the #24812 shape. Every remaining position is enumerated below, so the family needs no point cards.

Reader who acts: the domain:skills lane (SKILL.md's lane table: .claude/** is governed, Tier S). One PR on .claude/hooks/** and the matching *.selftest.sh; the seat lands it after an at-tier contract review. Filed by domain:skills seat 1 (seat post #7623, session_01M82feuetLzrCiBz41BJb7L) from two findings in the #24812 dev's report and PR #24886's Acceptance notes, which the contract review on PR #24886 (comment 6291929083, ③) escalated for filing (AGENTS.md Prime Directive 10). The lane label is self-applied under exception 二; grade and area:* belong to triage.

Dedupe (open and closed), objectstack, MCP search_issues (read 2026-10-20T13:28Z):

  • 「guard-shared-stash command substitution nested double quote false block」 → 5 hits;
  • 「guard-main-checkout-bash parameter expansion nested quote redirect false block」 → 7 hits.

The hits are #24812 (open, the sibling this card closes out) and the closed #11131, #10570, #11133, #10720, #11234 and #5883. Each of those is a different shape, and none covers these positions.

What happens

PR #24886 (#24812) gives guard-main-checkout-bash.sh one helper, subst_end(). Inside "…", a $(…) or a backtick opens its own quoting context, so a " in its body nests instead of closing the outer string. Two positions of the same family are still open.

  1. .claude/hooks/guard-shared-stash.sh split_segments() carries the identical pre-fix in-double-quote branch, with zero occurrences of subst_end at PR fix(hooks): read a command substitution inside a double-quoted word as its own quoting context #24886's head e4ba00dc9. Measured by the guard-main-checkout-bash.sh reads quotes inside a $(…) command substitution as closing the outer double quote, so a > inside a quoted curl body is taken for a redirect and a read-only command from the primary checkout is blocked #24812 dev at b1676441b:

    • echo "a $(echo "x; git stash pop")" is a pure read, and the guard answers exit 2 (blocked);
    • echo "x; git stash pop" answers 0.

    The inner " closes the outer string, so the ; inside the substitution splits a segment, and git stash pop reads as a command.

  2. A parameter expansion with a quoted default inside "…", in .claude/hooks/guard-main-checkout-bash.sh: echo "${x:-"a > b"}" answers exit 2 with target: b}, before and after PR fix(hooks): read a command substitution inside a double-quoted word as its own quoting context #24886. This is not a mechanical extension of subst_end(). Inside "${…}" a ' is literal and a " nests, so the expansion needs its own rule. The fix may instead be the header's existing promise: any target holding an expansion fails open.

Enumeration pin (every quote-aware segmenter in the hook directory)

git grep -nE '^(split_segments|tokenize|strip_line_comment|strip_heredocs)\(\)' origin/main -- .claude/hooks/ on origin/main ad8cf92f0 → 7 sites. The control, git grep -c split_segments on the same tree, hits 6 files.

file function status
guard-main-checkout-bash.sh strip_line_comment, split_segments, tokenize fixed by PR #24886 for $(…) / backticks; position 2 (${…}) open
guard-main-checkout-bash.sh strip_heredocs reads through strip_line_comment; covered by PR #24886's heredoc pin
guard-shared-stash.sh split_segments position 1, measured
guard-process-kill.sh split_segments NOT MEASURED: the PR measures it and fixes it or pins it clean
guard-tree-enum.sh split_segments NOT MEASURED: the PR measures it and fixes it or pins it clean

Done when

reach: measured hook verdicts on constructed pure reads (exit 2); no named producer yet for these two shapes. The class is the one #24812 named a producer for: cloud-session agents whose shell cwd resets to the primary checkout after every call.

Dedupe words: guard-shared-stash command substitution nested quote · hook parameter expansion quoted default false block · split_segments double quote expansion nesting


Generated by Claude Code

Activity

  1. objectstack-fleet commented on Oct 21, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 1
    Session: session_01M82feuetLzrCiBz41BJb7L
    Account: huangyiirene
    Branch: claude/issue-24901-hook-nested-quote-family
    Worktree: objectstack-issue-24901
    Domain: domain:skills
    Seat: domain:skills#1
    File surface: .claude/hooks/guard-shared-stash.sh, .claude/hooks/guard-main-checkout-bash.sh, .claude/hooks/guard-process-kill.sh, .claude/hooks/guard-tree-enum.sh and their *.selftest.sh (stop on breach; explain in the report)
    Container & model: M, mode:subagent, model: TIER_DEFAULT (dispatch-gates --tier .claude/hooks/guard-shared-stash.sh .claude/hooks/guard-main-checkout-bash.sh: "no path-derived mandate: the surface hits none of the 3 declared glob(s)"; a quoting rule per shape across four parsers is judgment work)
    Clause-②: no
    Responsibility: the hook scanners in .claude/hooks/**, owned by this lane, read a nested quote inside an expansion as closing the outer string | subst_end() in guard-main-checkout-bash.sh since PR #24886 covers $(…) and backticks in that hook only | any agent whose quoted command carries such a shape reaches it; measured exit 2 on constructed pure reads, no named producer yet
    Thread-read: none
    Serial constraints cleared: none — no open PR touches .claude/hooks/** (19 open PRs, file lists read 2026-10-21T00:52Z); the previous hook PR #24886 merged as 7e40e8a03, which is this card's base

    Landing path: .claude/** is Tier S. It needs an at-tier contract review on the head and the seat's ACCEPT; then the seat lands it. Posted 2026-10-21T00:54Z.


    Generated by Claude Code

  2. objectstack-fleet commented on Oct 21, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report

    {
      "issue": 24901,
      "status": "done",
      "branch": "claude/issue-24901-hook-nested-quote-family",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/25025",
      "session": "session_01M82feuetLzrCiBz41BJb7L (subagent: the parent's harness-stamped id, as carried by this branch's Claude-Session trailers)",
      "premise_still_valid": true,
      "summary": "Both card positions reproduced on 62bc12930 (stash `echo \"a $(echo \"x; git stash pop\")\"` exit 2; main-checkout-bash `echo \"${x:-\"a > b\"}\"` exit 2, target `b}`), and H4 measured both NOT MEASURED rows defective too (process-kill and tree-enum false-block the analogous shapes). Fix: guard-main-checkout-bash.sh's subst_end() now also tracks a quoted `${...}` (route A: one more closer on its stack; `\"`, `$(`, `${` and backticks nest, a backslash escapes, `}` closes) and owns the trigger itself (returns 1 for a non-opener), and guard-shared-stash.sh, guard-process-kill.sh and guard-tree-enum.sh carry a byte-identical copy that split_segments() delegates to. The three split_segments() are now byte-identical too; tree-enum gained the in-quote backslash branch its header already claimed (bounded in-place fix, same defect class, declared in the PR body). H2: copy pinned identical by each copying hook's self-test, chosen over a sourced helper because the hooks are standalone and already share by copy, and a helper would be a single point of failure for all four. H3: route B (rest inert after a quoted `${`) was prototyped and lets 6 measured real writes through that the pre-fix blocks, including `sed -i \"s/a/${x}/\" pkg/x.ts`; route A keeps them all blocked. H3 premise partly disproved: bash 5.2 reads `'...'` inside a quoted `${...}` as a quoted run when finding the brace (`\"${x:-'}'}\"` prints `'}'`), and bash --posix and dash read it as literal (they print `''}`). So a `'` run holding any of `\"` backslash `$` `}` backtick fails open, and any other run is skipped. H5: the PR body names the guard-main-checkout-bash.sh delta for the objectui port (objectui#13260).",
      "tests": "Hook matrices at 1df8c6399: guard-main-checkout-bash 157 passed / 0 failed (141 on main), guard-shared-stash 65/0 (53), guard-process-kill 80/0 (69), guard-tree-enum 50/0 (38), guard-main-checkout 120/0. Every pre-existing case passes unchanged. The new matrices against the pre-fix (62bc12930) hooks: 149/8, 59/6, 74/6, 43/7. Ablations through scripts/ablation-replace.mjs WRAP mode at d83fca2fb (hook files byte-identical to 1df8c6399); each mutation was proven on disk (anchor 1 to 0, blob changed), and each restore was proven by blob == HEAD and an empty git diff HEAD. A1 `${` arm removed: 8 red. A2 comment-scan delegation only: 2 red. A3 segmentation only: 8 red. A4 tokenisation only: 5 red. A5a bash-only reading of `'`: 1 red. A5b POSIX-only reading: 2 red. A5c `'` always fails open: 1 red (the recall case). A6 stash delegation removed: 5 red. A7 behaviour-neutral edit to the stash copy (locals reordered): exactly 1 red, the identity pin. A8 tree-enum in-quote escape branch deleted: 3 red. Differential corpus (41 commands x 4 hooks, pre vs post) is in the PR body; every recall control stays exit 2.",
      "gates": "Local, at 1df8c6399 (the merge head), each exit captured before any pipe: node scripts/check-closing-keyword-parity.mjs 0; node scripts/check-closing-keyword-parity.mjs --self-test 0; node scripts/check-comment-mask-corpus.mjs 0; node scripts/pm/check-harness-current.mjs --self-test 0; pnpm --filter @objectstack/lint run check:doc-formula-expressions 0 (first run at d83fca2fb answered exit 3 PREREQUISITE NOT MET; I built @objectstack/formula and @objectstack/lint under os-verify-lock, VERDICT command-exit 0, then it exited 0); pnpm check:agent-test-spelling 0; pnpm check:bash32-floor 0 ('19 constructs checked, floor bash 3.2'); pnpm check:cross-package-test-inputs 0; pnpm check:doc-authoring 0; pnpm check:driver-memory-census 0; pnpm check:gitlink-declared 0; pnpm check:nul-bytes 0; pnpm check:pm-governed-merges 0; pnpm check:refd-timer-probe 0; pnpm check:skill-frame-sync 0; pnpm check:watch-hint-literal 0. --ran: 'Run reconciliation — 16 derived, 16 run, 0 NOT-MEASURED, 0 UNRUN.' Also pnpm check:pm-settings-deny-roster 0, all five .claude/hooks/*.selftest.sh 0, and the control-byte self-scan found no match. CI on PR #25025 at report time: Check Changeset success, Check Documentation Links success, Check PR Size success, Test Core success, Dogfood Regression Gate success, 'The card this PR closes must claim this branch' success, 'No other open PR may claim the same issue' success, 'Part-of PR must not also close its card' success; in_progress: Governed Surface Queue Guard, Lint · CI, release & engine gates, Lint · ESLint, Lint · PM dispatch-gates, Lint · docs & code guards, Lint · repo & tooling gates (carries the hook self-test step), Lint · scoped · tooling self-tests, Type Check · consumer gates / debt ledger / source gates / workspace.",
      "line_budget": "No ceiling governs .claude/hooks/** (check-skill-line-ratchet header: '.claude/hooks/ and .claude/settings.json carry no ceiling'). Measured: 8 files, +392 / -34. guard-main-checkout-bash.sh 640 to 675, its selftest 412 to 443; guard-shared-stash.sh 232 to 292, selftest 203 to 240; guard-process-kill.sh 361 to 417, selftest 220 to 255; guard-tree-enum.sh 323 to 390, selftest 156 to 193.",
      "deviations": [
        "Dispatch constraint 'a real write the pre-fix hook blocks must stay blocked' is met for every pre-fix block that was a correct reading, but not for shapes the pre-fix blocked only through the misreading itself. Those are an action nested inside a quoted substitution reached through an inner string (`echo \"${x:-\"$(git stash pop)\"}\"`), a target holding an expansion (`echo hi > \"${x:-\"a > b\"}\"`), and a `'` run the shells disagree on. They are now documented fail-opens, and the PR body and open_questions carry this.",
        "tree-enum in-quote backslash branch added under the bounded in-place exemption (same defect class; mechanical, already pinned in its siblings; inside the claim's file surface; same gate family), declared in the PR body.",
        "subst_end() now owns its trigger (returns 1 for a non-opener), so guard-main-checkout-bash.sh's three call sites changed shape. The PR body's H5 section names this for the objectui port.",
        "Merged origin/main (05b28c199, 4 commits, none under .claude/hooks/) before opening, per AGENTS.md section 10, and re-ran the full local union on the merge head.",
        "Commit trailers use AGENTS.md's model-free pair (Claude-Session + 'Co-authored-by: Claude'), not the harness reminder's model-named Co-Authored-By line; the repo instruction takes precedence and the pre-push hook refuses a model id.",
        "The relay's read-back line names the assignee but prints no label line; the label was verified by a REST read of the pull instead (labels: [skip-changeset])."
      ],
      "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-process-kill.sh",
        ".claude/hooks/guard-process-kill.selftest.sh",
        ".claude/hooks/guard-tree-enum.sh",
        ".claude/hooks/guard-tree-enum.selftest.sh"
      ],
      "dogfood_battery": "NOT RUN: not applicable. The diff is .claude/hooks/** only, with no package, runtime or published surface, and it is not breaking. CI's Dogfood Regression Gate on the PR reported success.",
      "mcp_calls": "0",
      "api_writes": "4 REST writes, all through the fleet-write relay as objectstack-fleet[bot], in 2 relay strokes (each one POST /repos/objectstack-ai/objectstack/dispatches): POST /repos/objectstack-ai/objectstack/pulls (draft #25025); POST /repos/objectstack-ai/objectstack/issues/25025/assignees (huangyiirene); POST /repos/objectstack-ai/objectstack/issues/25025/labels (skip-changeset); POST /repos/objectstack-ai/objectstack/issues/24901/comments (this report). Plus 4 git pushes of the branch (not REST).",
      "open_questions": [
        {
          "question": "The three simple guards (stash, process-kill, tree-enum) and main-checkout-bash now treat a QUOTED substitution or expansion body as one inert word. A few real actions that the pre-fix blocked only by misreading an inner string as bare shell are therefore allowed, e.g. `echo \"${x:-\"$(git stash pop)\"}\"`, while `echo \"$(git stash pop)\"` was allowed before and after. Accept that as the documented fail-open, or claim quoted bodies?",
          "options": [
            "A: accept it as the documented fail-open. This is what this PR does: pinned in main-checkout-bash's matrix, with the class pinned by `echo \"$(git stash pop)\"` and `echo \"$(pkill -f node)\"`. It matches PR #24886's deliberate choice for `$(...)` and costs nothing more.",
            "B: analyse quoted substitution bodies as commands in the guards. This is a new claim with recursive segmentation (roughly +20 lines per copy), it diverges from main-checkout-bash's documented 'command inside a QUOTED $(...)' fail-open, and it widens what the guards block beyond the pre-fix behaviour."
          ],
          "recommendation": "A, because the pre-fix blocks in this group were accidents of the same misreading that produced the false blocks the card fixes. Keeping them would require keeping the misreading or adding a new claimed class, and the header's precision-over-recall contract prefers the fail-open."
        }
      ],
      "out_of_scope_findings": [
        "carrier: objectui#13260 · the objectui twins of all four hooks need this PR's delta: the subst_end() `${...}` arm, its `'` rule, the moved trigger, the three copies and the self-test identity pins. The PR body's H5 section names it · noted, not filed"
      ]
    }

    Generated by Claude Code

  3. objectstack-fleet commented on Oct 21, 2026

    @objectstack-fleet
    ContributorAuthor

    ACCEPT → PR #25025 comment 6306025148 · contract review PASS 6306009730 · domain:skills seat 1 · session_01M82feuetLzrCiBz41BJb7L · 2026-10-21T01:51Z


    Generated by Claude Code

  4. objectstack-fleet commented on Oct 21, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed · domain:skills seat 1 · session_01M82feuetLzrCiBz41BJb7L · 2026-10-21T05:16Z

    reading value
    PR #25025 merged: true at 2026-10-21T02:52Z, merge commit 338297e46 (queue entry 2026-10-21T01:53Z). Contract review PASS 6306009730 on head 1df8c6399
    origin/main 338297e46 fix(hooks): read $(…), backticks and ${…} inside "…" as their own quoting context in every Bash guard (#25025), under tip 63085c380

    State: closed completed by the PR's Fixes; pm:dispatched is removed in this act. Every row of this card's enumeration table is closed: all four Bash guards share one subst_end(), and each copy is pinned byte-identical by its own self-test. The director record on the PR (comment 6308493209) recognises the landing.

    Carried on the seat post, not filed:

    • Class C (contract review ①.6): adopt bash's reading of a ' run inside a quoted ${…} (the dev's A5a, one pin flips).
    • The stale "51 cases" count in guard-shared-stash.sh's header.

    The objectui port, objectstack-ai/objectui#13260, now owes this PR's delta as well; a memo is on that card.


    Generated by Claude Code

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

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions