T-SPBO-commit-helper-routes-through-tempfile
Status: closed/done · Impact: medium · Complexity: small
Auto-generated from a /sdlc:task-work post-mortem. Review and
promote to open/ready before picking up.
plugin/conventions/commit-messages.md documents the safe
mktemp + quoted-heredoc + git commit -F pattern, but it is
documentation only — every skill (and every ad-hoc operator) must
remember to follow it. When someone reaches for the obvious
git commit -m "<subject with (parens)>" instead, the message body’s
( ) are interpreted as command substitution under fish (and as a
glob under zsh), and the commit fails with a confusing git-usage
dump rather than a clear error. A small commit helper that always
routes the message through a tempfile turns the safe path into the
default path, removing the foot-gun instead of relying on recall.
Surfaced by T-1KPE-add-backlog-capture.
| Location | Role today |
|---|---|
plugin/conventions/commit-messages.md | Documents the canonical mktemp + quoted-'EOF' heredoc + git commit -F pattern and a Write-tool fallback. Prose only — nothing executable enforces routing through a tempfile; skills and operators must remember to do it. |
plugin/skills/*/SKILL.md (commit-authoring skills) | Reference the convention and hand-roll the mktemp + heredoc shape inline. A skill or operator that instead uses git commit -m "<msg with parens>" hits the failure the convention warns about. |
From T-1KPE-add-backlog-capture’s post-mortem: “
git commit -mwith()in the message body silently broke under the worktree’s fish shell (command substitution) — thegit commit -F <tempfile>convention works, but the failure mode is a confusing git-usage dump; a commit helper that always routes through a tempfile would remove the foot-gun.”
Proposed
Section titled “Proposed”A small commit helper (e.g. plugin/scripts/sdlc_commit.py or a
documented shell function) takes the commit subject/body as
arguments or on stdin — never as text the shell re-parses — writes it
to an mktemp’d file, and runs git commit -F. Skills that author
model-generated commit messages call the helper instead of hand-rolling
the heredoc, so the parens/substitution hazard cannot recur regardless
of the operator’s login shell (fish, zsh, bash). The
commit-messages.md convention points at the helper as the preferred
shape, keeping the hand-rolled heredoc documented as the
no-helper-available fallback.
Approach
Section titled “Approach”- Decide the helper shape. Candidates: (a) a Python script
plugin/scripts/sdlc_commit.pyreading the message from--message -/stdin or--subject/--bodyflags (no shell re-parse), thengit commit -F <mktemp>; (b) a documented shell function in the convention. Prefer (a) — argv/stdin never round-trips through command substitution, and it works identically under fish/zsh/bash. Decide whether the helper forwards passthrough flags (--amend,-C <path>,--no-verify). - Implement the helper with
mktemp, write-then-git commit -F, and a clear non-zero exit + message ongitfailure (not a usage dump). - Point
plugin/conventions/commit-messages.mdat the helper as the preferred shape; keep the heredoc + Write-tool fallbacks documented. Optionally grep-update the highest-traffic commit-authoring skills to call the helper. - Add a fixture/eval: a commit message containing literal
()$and backticks lands verbatim via the helper under a shell where the naive-mform would break.
Files to touch
Section titled “Files to touch”| Location | Kind | Change |
|---|---|---|
plugin/scripts/sdlc_commit.py | new | Commit helper: reads message from argv/stdin, writes to mktemp, runs git commit -F; clear error on failure. (Path/shape per Approach step 1.) |
plugin/conventions/commit-messages.md | modify | Point at the helper as the preferred shape; keep heredoc / Write-tool fallbacks documented. |
Acceptance criteria
Section titled “Acceptance criteria”- AC-1: The helper commits a message containing literal
(),$, and backticks verbatim — no command substitution, no glob expansion — under fish, zsh, and bash. - AC-2: A
gitfailure inside the helper surfaces a clear, one-line error (and a non-zero exit), not a rawgit-usage dump. - AC-3:
plugin/conventions/commit-messages.mdnames the helper as the preferred routing and still documents the hand-rolled heredoc / Write-tool fallbacks for contexts where the helper isn’t reachable.
Out of scope
Section titled “Out of scope”- Forcing every existing skill to migrate to the helper in this task — the convention update plus the helper is the deliverable; bulk skill-by-skill migration can follow.
- Replacing the documented Write-tool sandbox fallback — that path
stays for sandboxes that deny the helper’s
Bashverbs.
Dependencies
Section titled “Dependencies”- none
Discovery context
Section titled “Discovery context”Spawned by /sdlc:task-work post-mortem of T-1KPE-add-backlog-capture on 2026-05-27.
Dedup search (spawn-from-post-mortem)
Section titled “Dedup search (spawn-from-post-mortem)”Bullet: git commit -m with () in the message body silently broke under the worktree’s fish shell
(command substitution) — the git commit -F
- 36 / closed/done / 2026-05-22-move-plugin-runtime-state-to-sdlc-dir — Migrate sdlc plugin runtime state from .claude/ to .sdlc/
- 32 / closed/superseded / 2026-05-25-task-work-step7-explicit-baseline-dir — task-work Step 7 must pass —baseline-dir explicitly to defeat silent fallback in worktree
- 31 / planning/draft / 2026-05-22-task-work-uses-worktree-skill-md — task-work loads SKILL.md from the worktree, not ${CLAUDE_PLUGIN_ROOT}
- 30 / closed/done / 2026-05-20-orchestrator-categorized-in-flight-limits — Replace orchestrator parallelism cap with categorized in-flight limits configurable in sdlc.yaml
- 29 / planning/draft / 2026-05-22-worktree-move-creates-parent-dir — Document or auto-create destination parent dir for git worktree move Decision: SPAWNED Rationale: Confirmed SPAWNED. All top candidates are incidental keyword overlap on “worktree”/“silently”; none concern commit-message shell safety. The two prior commit-message tasks (2026-05-19-document-commit-message-pattern #21, 2026-05-20-investigate-sandbox-multiline-commit-denial #64) are both closed/done and documented the pattern — this task ships an executable helper that makes the safe routing the default, a distinct deliverable.
Post-mortem
Section titled “Post-mortem”Captured by /sdlc:task-work on 2026-06-04. PR: pending.
Acceptance criteria coverage
Section titled “Acceptance criteria coverage”- AC-1: auto + agent-manual —
bun test plugin/scripts/tests/sdlc_commit.test.tsasserts a message with literal()$and backticks lands verbatim via both--subject/--body(argv) and--message -(stdin); additionally drove the helper frombash,zsh, andfishagainst a throwaway repo and confirmed byte-identical subject/body in each shell. The implementation commit itself was authored by the helper with(parens),$, and backticks in the body — verified verbatim ingit log -1 --format=%b. - AC-2: auto — the suite’s “git failure” case unstages everything and asserts
the helper exits
1withsdlc_commit: git commit failed (exit N)plus git’s own “nothing to commit” detail relayed, not a raw-musage dump. - AC-3: agent-manual —
plugin/conventions/commit-messages.mdnow opens with a “Preferred shape: thesdlc_commithelper” section; the hand-rolled heredoc and Write-tool paths are retained verbatim as documented fallbacks.
What worked
Section titled “What worked”- The implementation-ready gate, touchpoint parser, and placeholder scanner all passed first try against the spec — no definition gap to fill.
- Dogfooding the helper to author its own commits was the cleanest possible end-to-end proof: the shell-hazard chars in the commit body are the exact failure mode the task exists to remove.
Friction and automation gaps
Section titled “Friction and automation gaps”- Spec named the helper as
plugin/scripts/sdlc_commit.py, but the project is fully TypeScript (plugin/scripts/has 23.ts, zero.py; D0006/M0002 en-masse port). Implemented assdlc_commit.tsto match the substrate. The spec’s## Files to touchrow went stale against the language migration — task-review / ensure-ready could flag a.pyFiles-to-touch path in a TS-onlyplugin/scripts/tree as a likely-stale citation. → T-A7MY-ensure-ready-flags-stale-language-touchpoint - Step 7’s
--diff-against-baselineflagged 8 spuriousnew-drift:lines that were- OKaudit_entitieslines for other tasks’ files (T-0RW4, T-132J, …) — none in this branch’s diff vsorigin/main. Root cause: the Step 3a baseline was captured from the main repo’s working tree, which carried unrelated uncommitted edits to those task files, while the worktree gates against a clean HEAD checkout; the two corpora differ, so the differ reports the clean lines as “new.” The Step 9 ancestry rebase did NOT clear it (it drops foreign task-state commits, not working-tree corpus skew). Verified the branch introduces zero real findings:audit_entities.tsreports all three task files this branch touches as- OK, and the 5 real DRIFT files are all in the 356-finding pre-existing baseline. Capturing the baseline from a clean tree at the worktree’s actual branch-point (not the main repo’s dirty working tree) would avoid the false positive. → T-TWZD-normalize-baseline-diff-nondeterministic-output - The quality gate’s
--baseline-dirdefaults to the worktree’s.sdlc/, but Step 3a writes the baseline to the main repo’s.sdlc/; the gate failedbaseline not founduntil--baseline-dirwas pointed back at the main repo explicitly. task-work Step 7 should pass--baseline-dir <main-repo>/.sdlc/quality-baselinesby default, mirroring Step 3a’s write location, so the worktree-vs-main path skew can’t surface. → T-44OO-plugin-scripts-self-discover-project-root
Spawned follow-up tasks
Section titled “Spawned follow-up tasks”- T-A7MY-ensure-ready-flags-stale-language-touchpoint (https://github.com/sksizer/dev/pull/279) — spawned: flag a Files-to-touch / Today path whose extension is anomalous for its directory’s language.
- T-TWZD-normalize-baseline-diff-nondeterministic-output — linked: same baseline-diff false-positive class (spurious new-drift from non-deterministic line content).
- T-44OO-plugin-scripts-self-discover-project-root — linked: the
--baseline-dirworktree-vs-main silent-fallback the gate tripped on.