Read-only guard rejects subagent-stop handoff with pipe chars; Runner records error #543

Closed
opened 2026-09-08 20:55:44 +00:00 by jared · 3 comments
Owner

When an os-sdlc agent's subagent-stop call includes a handoff body containing pipe characters (|), the Bash pre-hook read-only command guard rejects it repeatedly. The agent then submits a minimal call with no handoff body. The Runner records the dispatch as error and the implementation settles to implementation_failed with reason verdict:error, despite the agent completing its work.

Symptoms:

  • subagent-stop calls with long handoff bodies containing shell metacharacters (particularly |) are rejected by the read-only guard
  • Agent falls back to minimal call without handoff body
  • Runner dispatch marked as error instead of success
  • Implementation verdict incorrectly set to verdict:error

Expected behavior:

  • subagent-stop should accept handoff via --body-file or stdin so shell-metachar content never hits the guard
  • Runner should not record error when agent completed its work correctly

Occurrence:

  • Implementation: hyperthrive-websites ticket #38, implementation_id 2, dispatch 12
  • Session: 7f892a4b-e70a-44b2-acad-d64dbbcf0a10
  • Date: 2026-09-08
  • Agent: os-sdlc message-tracer

Related:

  • cc-os #536 (os-sdlc gate broken in worktrees) — mention as related, this is a distinct failure

Discoverer: hyperthrive-websites ticket #38, session 7f892a4b-e70a-44b2-acad-d64dbbcf0a10, 2026-09-08. Read-only guard shell-metachar rejection chain prevents proper handoff delivery during message-tracer dispatch.

When an os-sdlc agent's subagent-stop call includes a handoff body containing pipe characters (|), the Bash pre-hook read-only command guard rejects it repeatedly. The agent then submits a minimal call with no handoff body. The Runner records the dispatch as `error` and the implementation settles to `implementation_failed` with reason `verdict:error`, despite the agent completing its work. **Symptoms:** - subagent-stop calls with long handoff bodies containing shell metacharacters (particularly `|`) are rejected by the read-only guard - Agent falls back to minimal call without handoff body - Runner dispatch marked as `error` instead of success - Implementation verdict incorrectly set to `verdict:error` **Expected behavior:** - subagent-stop should accept handoff via `--body-file` or stdin so shell-metachar content never hits the guard - Runner should not record `error` when agent completed its work correctly **Occurrence:** - Implementation: hyperthrive-websites ticket #38, implementation_id 2, dispatch 12 - Session: 7f892a4b-e70a-44b2-acad-d64dbbcf0a10 - Date: 2026-09-08 - Agent: os-sdlc message-tracer **Related:** - cc-os #536 (os-sdlc gate broken in worktrees) — mention as related, this is a distinct failure -------- **Discoverer:** hyperthrive-websites ticket #38, session 7f892a4b-e70a-44b2-acad-d64dbbcf0a10, 2026-09-08. Read-only guard shell-metachar rejection chain prevents proper handoff delivery during message-tracer dispatch.
Author
Owner

Correction plan (2026-09-10, session a29eac7b)

Diagnosis

Two records collided:

  • #501 (commit 32e7c7c, 2026-08-24) moved every settling charter to printf ... > /tmp/handoff-N.json, then subagent-stop < file, then rm file, because the echo-pipe form corrupted newlines under zsh and tripped the harness worktree rule.
  • ADR-0156 (commit 63e06f6, 2026-08-26) made judge roles deny-on-any-mutation in plugins/os-sdlc/lib/os_sdlc/bash_guard.rb. > and rm are both mutation evidence. The #501 snippet has been illegal for judge roles since that day.

Second defect: BashGuard::REDIRECT scans the raw command string with no quote awareness. Any > inside the quoted JSON payload (-> in MESSAGE lines, => or << in quoted Ruby) reads as a redirect. Same class as the | symptom in this ticket. Pipe or file, the payload will always contain shell metacharacters.

Reproduced 2026-09-10: implementation 8 of #528, change-auditor dispatch 136, refusals (redirect) and (rm); an empty subagent-stop closed the run with verdict:error. Do not fix by changing the -> notation; the next handoff trips on a different character.

ADR check (os-adr:find): quote-aware scanning is a false-positive fix to an already-permitted command; no new ADR, cite ADR-0156. Charter change from file to pipe stays within ADR-0144 ("pipe to subagent-stop"). Do NOT add a trusted-sink carve-out for os-sdlc-runner without amending ADR-0156.

Steps

  1. Red test, guard. plugins/os-sdlc/tests/bash_guard_test.rb (create if absent). For judge role change-auditor, assert deny? is false for:
    • printf '%s' '{"content": "MESSAGE: Cart -> Pricing#x"}' | os-sdlc-runner subagent-stop
    • printf '%s' '{"content": "a | b => c << d"}' | os-sdlc-runner subagent-stop
    • grep -n "=>" lib/foo.rb
      Assert deny? stays true for: printf x > /tmp/f, rm /tmp/f, sed -i s/a/b/ f, echo x | tee f, cat f 2>&1 >out. Assert a > inside quotes followed by a real > out after the quotes is still denied.
  2. Green, guard. In bash_guard.rb, add a private scannable that replaces every single- and double-quoted span with an empty string (regex /'[^']*'|"(?:\\.|[^"\\])*"/), then use scannable instead of @command in segments and redirect_targets. Run mutating_heads, in_place_edit?, and write_targets on the stripped string too, so "rm" inside a quoted payload is not a head. Do not attempt full shell parsing; this stays best-effort per ADR-0156.
  3. Red test, charter shape. Add an assertion (docs test in the plugins/os-sdlc/tests/all.rb style) that no file under plugins/os-sdlc/agents/ contains /tmp/handoff-, subagent-stop <, or rm /tmp/handoff.
  4. Green, charters. In every settling charter (the eleven from #501 minus the three merged by ADR-0170, plus change-auditor.md, plus suite-check.md, wiring.md, lint-fixer.md if they settle), replace the three-command block with one command:
    printf '%s' '{"dispatch_id": <N>, "session_id": "<from your dispatch prompt>", "agent_type": "<name>", "content": "<yaml handoff as a JSON string>"}' | os-sdlc-runner subagent-stop
    
    Keep the note that dispatch_id alone resolves the dispatch. Drop "run the three commands separately". Add: "Use printf, never echo. Wrap the JSON in single quotes. Escape a single quote inside the YAML as '''." Check plugins/os-sdlc/lib/os_sdlc/runner/round_commands.rb (touched by 32e7c7c) for the same snippet in generated briefs.
  5. Harness check, live. In an isolated worktree session, dispatch one judge (seam-designer is cheapest) and confirm the single pipe passes both the judge guard and the harness "too complex to verify" worktree rule. The Claude Code changelog says that rule was relaxed for pipes and heredocs that never touch the VCS; verify, do not assume. A herestring (<<<) is a redirect and is NOT an allowed fallback. If the harness still refuses, file a harness ticket and take the trusted-sink option with an ADR-0156 amendment.
  6. Suite and lint. ruby plugins/os-sdlc/tests/all.rb; rubocop on the two Ruby files. Baseline 1118 runs green as of 166bc13.
  7. Records. One History row in the os-sdlc leaf under docs/implementation-status/. Run bin/refresh-plugins. No ADR unless step 5 forces the trusted-sink path.
  8. Close. os-backlog close 543 --done ... --evidence ... --follow-ups .... Then remove waiting from #528 and rerun /os-sdlc:implement #528 from a fresh implementation-open.

Follow-ups to record at close

  • Why some judge handoffs passed on 2026-09-09 (24 of 24 in run d233a2f7) while the same snippet failed on 09-08 and 09-10 is UNKNOWN. Note it; do not chase unless it recurs after this fix.
## Correction plan (2026-09-10, session a29eac7b) ### Diagnosis Two records collided: - #501 (commit 32e7c7c, 2026-08-24) moved every settling charter to `printf ... > /tmp/handoff-N.json`, then `subagent-stop < file`, then `rm file`, because the echo-pipe form corrupted newlines under zsh and tripped the harness worktree rule. - ADR-0156 (commit 63e06f6, 2026-08-26) made judge roles deny-on-any-mutation in `plugins/os-sdlc/lib/os_sdlc/bash_guard.rb`. `>` and `rm` are both mutation evidence. The #501 snippet has been illegal for judge roles since that day. Second defect: `BashGuard::REDIRECT` scans the raw command string with no quote awareness. Any `>` inside the quoted JSON payload (`->` in MESSAGE lines, `=>` or `<<` in quoted Ruby) reads as a redirect. Same class as the `|` symptom in this ticket. Pipe or file, the payload will always contain shell metacharacters. Reproduced 2026-09-10: implementation 8 of #528, change-auditor dispatch 136, refusals `(redirect)` and `(rm)`; an empty subagent-stop closed the run with `verdict:error`. Do not fix by changing the `->` notation; the next handoff trips on a different character. ADR check (os-adr:find): quote-aware scanning is a false-positive fix to an already-permitted command; no new ADR, cite ADR-0156. Charter change from file to pipe stays within ADR-0144 ("pipe to subagent-stop"). Do NOT add a trusted-sink carve-out for `os-sdlc-runner` without amending ADR-0156. ### Steps 1. **Red test, guard.** `plugins/os-sdlc/tests/bash_guard_test.rb` (create if absent). For judge role `change-auditor`, assert `deny?` is false for: - `printf '%s' '{"content": "MESSAGE: Cart -> Pricing#x"}' | os-sdlc-runner subagent-stop` - `printf '%s' '{"content": "a | b => c << d"}' | os-sdlc-runner subagent-stop` - `grep -n "=>" lib/foo.rb` Assert `deny?` stays true for: `printf x > /tmp/f`, `rm /tmp/f`, `sed -i s/a/b/ f`, `echo x | tee f`, `cat f 2>&1 >out`. Assert a `>` inside quotes followed by a real `> out` after the quotes is still denied. 2. **Green, guard.** In `bash_guard.rb`, add a private `scannable` that replaces every single- and double-quoted span with an empty string (regex `/'[^']*'|"(?:\\.|[^"\\])*"/`), then use `scannable` instead of `@command` in `segments` and `redirect_targets`. Run `mutating_heads`, `in_place_edit?`, and `write_targets` on the stripped string too, so `"rm"` inside a quoted payload is not a head. Do not attempt full shell parsing; this stays best-effort per ADR-0156. 3. **Red test, charter shape.** Add an assertion (docs test in the `plugins/os-sdlc/tests/all.rb` style) that no file under `plugins/os-sdlc/agents/` contains `/tmp/handoff-`, `subagent-stop <`, or `rm /tmp/handoff`. 4. **Green, charters.** In every settling charter (the eleven from #501 minus the three merged by ADR-0170, plus `change-auditor.md`, plus `suite-check.md`, `wiring.md`, `lint-fixer.md` if they settle), replace the three-command block with one command: ``` printf '%s' '{"dispatch_id": <N>, "session_id": "<from your dispatch prompt>", "agent_type": "<name>", "content": "<yaml handoff as a JSON string>"}' | os-sdlc-runner subagent-stop ``` Keep the note that `dispatch_id` alone resolves the dispatch. Drop "run the three commands separately". Add: "Use printf, never echo. Wrap the JSON in single quotes. Escape a single quote inside the YAML as '\''." Check `plugins/os-sdlc/lib/os_sdlc/runner/round_commands.rb` (touched by 32e7c7c) for the same snippet in generated briefs. 5. **Harness check, live.** In an isolated worktree session, dispatch one judge (seam-designer is cheapest) and confirm the single pipe passes both the judge guard and the harness "too complex to verify" worktree rule. The Claude Code changelog says that rule was relaxed for pipes and heredocs that never touch the VCS; verify, do not assume. A herestring (`<<<`) is a redirect and is NOT an allowed fallback. If the harness still refuses, file a harness ticket and take the trusted-sink option with an ADR-0156 amendment. 6. **Suite and lint.** `ruby plugins/os-sdlc/tests/all.rb`; rubocop on the two Ruby files. Baseline 1118 runs green as of 166bc13. 7. **Records.** One History row in the os-sdlc leaf under `docs/implementation-status/`. Run `bin/refresh-plugins`. No ADR unless step 5 forces the trusted-sink path. 8. **Close.** `os-backlog close 543 --done ... --evidence ... --follow-ups ...`. Then remove `waiting` from #528 and rerun `/os-sdlc:implement #528` from a fresh implementation-open. ### Follow-ups to record at close - Why some judge handoffs passed on 2026-09-09 (24 of 24 in run d233a2f7) while the same snippet failed on 09-08 and 09-10 is UNKNOWN. Note it; do not chase unless it recurs after this fix.
Author
Owner

Work started 2026-09-10 (session 8d211345), branch ticket-543 from main a7af886. Implementing plan steps 1-4 and 6 (quote-aware BashGuard scanning, single-pipe handoff in charters). Step 5 live harness check to follow.

Work started 2026-09-10 (session 8d211345), branch ticket-543 from main a7af886. Implementing plan steps 1-4 and 6 (quote-aware BashGuard scanning, single-pipe handoff in charters). Step 5 live harness check to follow.
Author
Owner

Resolution

Done: BashGuard strips single- and double-quoted spans before scanning for redirects and mutating heads (QUOTED_SPAN + private scannable in plugins/os-sdlc/lib/os_sdlc/bash_guard.rb). The ten settling charters under plugins/os-sdlc/agents/ replace the three-command temp-file handoff with one printf pipe to subagent-stop; a docs test forbids the old snippet. No trusted-sink carve-out; ADR-0156 unchanged.

Evidence: main 93f1595 (commits 07bb036 + docs row). Suite 1149 runs, 0 failures. Live check 2026-09-10: a real os-sdlc:seam-designer agent ran the single pipe with ->, |, =>, <<, and a quoted rm in the payload; the pre_tool_use guard allowed it and the runner replied 'subagent-stop no dispatch 999999' (exit 3, expected for a fake id). Limit: the session ran with permissions bypassed, so the harness worktree 'too complex to verify' rule was not exercised; the next /os-sdlc:implement run is the first test of it under default permissions.

Follow-ups: none. Noted per plan: the 24/24 judge handoffs that passed on 2026-09-09 while the same snippet failed on 09-08 and 09-10 remain unexplained; do not chase unless it recurs.

## Resolution **Done:** BashGuard strips single- and double-quoted spans before scanning for redirects and mutating heads (QUOTED_SPAN + private scannable in plugins/os-sdlc/lib/os_sdlc/bash_guard.rb). The ten settling charters under plugins/os-sdlc/agents/ replace the three-command temp-file handoff with one printf pipe to subagent-stop; a docs test forbids the old snippet. No trusted-sink carve-out; ADR-0156 unchanged. **Evidence:** main 93f1595 (commits 07bb036 + docs row). Suite 1149 runs, 0 failures. Live check 2026-09-10: a real os-sdlc:seam-designer agent ran the single pipe with ->, |, =>, <<, and a quoted rm in the payload; the pre_tool_use guard allowed it and the runner replied 'subagent-stop no dispatch 999999' (exit 3, expected for a fake id). Limit: the session ran with permissions bypassed, so the harness worktree 'too complex to verify' rule was not exercised; the next /os-sdlc:implement run is the first test of it under default permissions. **Follow-ups:** none. Noted per plan: the 24/24 judge handoffs that passed on 2026-09-09 while the same snippet failed on 09-08 and 09-10 remain unexplained; do not chase unless it recurs.
jared closed this issue 2026-09-10 17:49:06 +00:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
jared/cc-os#543
No description provided.