Skip to content

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.

LocationRole today
plugin/conventions/commit-messages.mdDocuments 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 -m with ( ) in the message body silently broke under the worktree’s fish shell (command substitution) — the git 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.”

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.

  1. Decide the helper shape. Candidates: (a) a Python script plugin/scripts/sdlc_commit.py reading the message from --message -/stdin or --subject/--body flags (no shell re-parse), then git 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).
  2. Implement the helper with mktemp, write-then-git commit -F, and a clear non-zero exit + message on git failure (not a usage dump).
  3. Point plugin/conventions/commit-messages.md at 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.
  4. Add a fixture/eval: a commit message containing literal ( ) $ and backticks lands verbatim via the helper under a shell where the naive -m form would break.
LocationKindChange
plugin/scripts/sdlc_commit.pynewCommit 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.mdmodifyPoint at the helper as the preferred shape; keep heredoc / Write-tool fallbacks documented.
  • 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 git failure inside the helper surfaces a clear, one-line error (and a non-zero exit), not a raw git-usage dump.
  • AC-3: plugin/conventions/commit-messages.md names the helper as the preferred routing and still documents the hand-rolled heredoc / Write-tool fallbacks for contexts where the helper isn’t reachable.
  • 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 Bash verbs.
  • none

Spawned by /sdlc:task-work post-mortem of T-1KPE-add-backlog-capture on 2026-05-27.

Bullet: git commit -m with () in the message body silently broke under the worktree’s fish shell (command substitution) — the git commit -F 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. Keywords searched: substitution, convention, confusing, git-usage, silently, worktree, tempfile, foot-gun Excluded: 2026-05-27-add-backlog-capture Top candidates (score / status / headline):

  • 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.

Captured by /sdlc:task-work on 2026-06-04. PR: pending.

  • AC-1: auto + agent-manual — bun test plugin/scripts/tests/sdlc_commit.test.ts asserts a message with literal ( ) $ and backticks lands verbatim via both --subject/--body (argv) and --message - (stdin); additionally drove the helper from bash, zsh, and fish against 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 in git log -1 --format=%b.
  • AC-2: auto — the suite’s “git failure” case unstages everything and asserts the helper exits 1 with sdlc_commit: git commit failed (exit N) plus git’s own “nothing to commit” detail relayed, not a raw -m usage dump.
  • AC-3: agent-manual — plugin/conventions/commit-messages.md now opens with a “Preferred shape: the sdlc_commit helper” section; the hand-rolled heredoc and Write-tool paths are retained verbatim as documented fallbacks.
  • 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.
  • 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 as sdlc_commit.ts to match the substrate. The spec’s ## Files to touch row went stale against the language migration — task-review / ensure-ready could flag a .py Files-to-touch path in a TS-only plugin/scripts/ tree as a likely-stale citation. → T-A7MY-ensure-ready-flags-stale-language-touchpoint
  • Step 7’s --diff-against-baseline flagged 8 spurious new-drift: lines that were - OK audit_entities lines for other tasks’ files (T-0RW4, T-132J, …) — none in this branch’s diff vs origin/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.ts reports 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-dir defaults to the worktree’s .sdlc/, but Step 3a writes the baseline to the main repo’s .sdlc/; the gate failed baseline not found until --baseline-dir was pointed back at the main repo explicitly. task-work Step 7 should pass --baseline-dir <main-repo>/.sdlc/quality-baselines by 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

← Back to Tasks