crease-data/crease#276 (merged) pinned every supply-chain surface in crease and hardened every shell that gates its build. Six of its findings apply to what the kit ships into a target, and none has a kit home. Four are now applied to crease's copies of the kit templates in crease-data/crease#281 (open), each guarded by a crease test that was red first. Two (the mise and MCP templates) are templates crease does not carry, so their patches are inline below.
1. printf … | grep -q under pipefail can read a match as no match (the review-bot templates)
In printf '%s' "$x" | grep -q y, grep exits on its first match, and bash's printf writes a multi-line subject one write() per line. The next write takes SIGPIPE, and pipefail turns the match into a 141 that the if reads as no match. Measured in crease-data/crease#276: 5 flips in 20,000 runs at 22 bytes pinned to one CPU, and deterministic flips above ~49 KiB. A crafted 72 KB label set flipped claude.yml's skip-claude gate. cbk-conventions-reference.md § Hook authoring already states the trap for hooks. The kit's templates still pipe:
claude-review.yml: the deep and fast label checks (printf '%s' "$labels" | grep -qF …);
claude.yml: the skip-claude check;
claude-review.yml's label-removal step: gh pr view … --jq … | grep -qxF 'claude-review-again'. A failed gh inside that pipe read as "Label already removed (benign)", against the step's own comment that every other failure is an error.
Fix: here-strings, and the label read captured first.
names="$(gh pr view "$PR_NUMBER" --repo "$REPO" --json labels --jq '.labels[].name')" \
|| { echo "::error::Could not read the PR's labels."; exit 1; }
if grep -qxF 'claude-review-again' <<<"$names"; then
…
if grep -qF '"claude-deep-review"' <<<"$labels"; then
…
elif grep -qF '"skip-claude"' <<<"$ALL_LABELS"; then
Driven with a fake gh through the removal step's three branches: present, the edit runs; absent, the benign line; gh fails, exit 1 with the error. Crease commit da14f3f.
2. An unset shell: runs bash -e {0}, with no pipefail
GitHub's shell table: unspecified on Linux is bash -e {0}, and an explicit shell: bash is bash --noprofile --norc -eo pipefail {0} ("Note that this runs a different command to when bash is specified explicitly", docs.github.com workflow syntax, read 2026-09-29). Without pipefail, cmd | tee log passes when cmd fails. The two review templates and tooling.md's CI skeleton set no shell.
Fix: defaults: run: shell: bash in both review templates, the CI skeleton, and scaffold's starter CI stub. Crease commits 7f46fa3, 86da5e1.
3. runs-on: ubuntu-latest moves when GitHub re-points the label
A runner-image change then lands in every target unreviewed. It does not break required checks, which match on job name.
Fix: runs-on: ubuntu-24.04 with the reason, in the two review templates, the CI skeleton and the starter stub. Crease commit 86da5e1.
4. The kit says no bot covers a container base image; Dependabot covers most, with three gaps
§ Dependency settle-window, tooling.md sanity question 6b and the starter's "Not covered by any bot" line (which sits right under a commented docker ecosystem stub) all say a container base-image tag is outside every update bot. For most images it is not. Dependabot's docker and docker-compose ecosystems bump a pinned repo:tag@sha256 tag and digest together, and the docs' cooldown table lists both as supporting default-days. The three real gaps, measured in crease-data/crease#276 and checked against dependabot-core's source (read 2026-09-29):
- the docker updater reads
FROM lines in a Dockerfile (and Kubernetes and Helm image references in YAML), never COPY --from=<image>. Route such an image through a named FROM … AS <stage>, as crease's fastapi image does for uv;
- the
devcontainers ecosystem bumps a devcontainer's Features, not its image (devcontainers/…/file_parser.rb's parse calls only parse_features). A digest-pinned devcontainer image is hand-bumped. crease's own crease-data/crease#276 docs claimed otherwise and are now corrected;
- its cooldown takes a publication date only from Docker Hub ("Docker Hub's
tag_last_pushed is currently the only accepted source", dependabot-core docker/README.md, read 2026-09-25). So a GHCR, quay.io or MCR bump gets its release-age check by hand.
Fix: all three surfaces say an image in a Dockerfile FROM or a compose image: is covered, and name the three gaps. Crease commits 712bdd9, 0b3177a.
5. mise runs inline tasks under sh -c -o errexit (dash), with no pipefail (template not carried in crease)
tooling.md offers mise as a task runner. mise's default inline shell is sh -c -o errexit, and on Debian/Ubuntu sh is dash, so there is no pipefail. crease's fix (commit b76adbf on crease-data/crease#276), for the kit's mise template:
# Every inline task runs under bash with errexit AND pipefail. mise's default is `sh -c -o errexit`,
# which is dash on Debian/Ubuntu: no pipefail. inherit_errexit keeps a failing `$(...)` fatal as it
# is under dash; bash drops errexit inside command substitution without it.
# - Needs mise >= 2026.7.15 (jdx/mise#11354, the config-scoped default shell). An older mise prints
# `WARN unknown field ... task_config.shell` and stays on dash.
# - A project `[settings] unix_default_inline_shell_args` is no alternative: it is global-only and
# ignored from project config since 2026.7.14 (jdx/mise#11293, security).
[task_config]
shell = "bash -O inherit_errexit -c -o errexit -o pipefail"
crease's tests/test_shell_pipefail.py fails the build if the shell drops pipefail or a task overrides it.
6. Unpinned stdio MCP servers run network-fetched code at every session start (template not carried in crease)
.mcp.json.example launches npx -y @modelcontextprotocol/server-github, …-linear and …-time with no version. That fetches whatever is newest at every session start, outside every lockfile and the settle window. crease pins each server to an exact version past the window (@modelcontextprotocol/server-github@2025.4.8, @upstash/context7-mcp@4.1.1, mcp-server-time@2026.8.18, …). tests/test_supply_chain_pins.py::test_every_stdio_mcp_server_runs_a_pinned_version fails on an unpinned npx/uvx server. Crease commit 344a46a on crease-data/crease#276. For the kit: pin the example's servers, and say in the settle-window section that an MCP server is a dependency.
Where it lands in the kit
| Kit file |
Item |
crease commit |
blueprint/references/templates/claude-review.yml |
1, 2, 3 |
da14f3f, 7f46fa3, 86da5e1, 293f03b (the removal step's comment: a label deleted from the repository is the benign branch) |
blueprint/references/templates/claude.yml |
1, 2, 3 |
same |
blueprint/references/templates/tooling.md (CI skeleton; 6b) |
2, 3, 4 |
7f46fa3, 86da5e1, 712bdd9 |
scaffold/references/github-starter-templates.md (the CI stub; "not covered by any bot") |
2, 3, 4 |
86da5e1, 712bdd9 |
cbk-conventions-reference.md § Dependency settle-window |
4, 6 |
712bdd9 |
the mise template; .mcp.json.example |
5, 6 |
inline above |
crease's tests/test_shell_pipefail.py and tests/test_supply_chain_pins.py now read the kit templates as well as crease's workflows (commit 38fd348, red first). For the kit itself, the same three assertions are a kit sub-block check in crease's verification block (commit d4bf867):
tpl=.claude/skills/blueprint/references/templates
absent grep -rnE "^[[:space:]]*runs-on:[[:space:]]*ubuntu-latest" .claude/skills/
for t in "$tpl/claude-review.yml" "$tpl/claude.yml" "$tpl/tooling.md" .claude/skills/scaffold/references/github-starter-templates.md; do
grep -qE '^[[:space:]]*shell: bash[[:space:]]*$' "$t" || { echo "$t sets no \`defaults: run: shell: bash\` for its workflow"; exit 1; }
done
absent grep -nE '^[^#]*\|[[:space:]]*grep -[A-Za-z]*q' "$tpl/claude-review.yml" "$tpl/claude.yml"
Three mutants of the review template (ubuntu-latest, shell: sh, a label check piped back through printf) each turn it red.
crease-data/crease#276 (merged) pinned every supply-chain surface in crease and hardened every shell that gates its build. Six of its findings apply to what the kit ships into a target, and none has a kit home. Four are now applied to crease's copies of the kit templates in crease-data/crease#281 (open), each guarded by a crease test that was red first. Two (the mise and MCP templates) are templates crease does not carry, so their patches are inline below.
1.
printf … | grep -qunder pipefail can read a match as no match (the review-bot templates)In
printf '%s' "$x" | grep -q y, grep exits on its first match, and bash's printf writes a multi-line subject onewrite()per line. The next write takes SIGPIPE, and pipefail turns the match into a 141 that theifreads as no match. Measured in crease-data/crease#276: 5 flips in 20,000 runs at 22 bytes pinned to one CPU, and deterministic flips above ~49 KiB. A crafted 72 KB label set flippedclaude.yml's skip-claude gate.cbk-conventions-reference.md§ Hook authoring already states the trap for hooks. The kit's templates still pipe:claude-review.yml: the deep and fast label checks (printf '%s' "$labels" | grep -qF …);claude.yml: the skip-claude check;claude-review.yml's label-removal step:gh pr view … --jq … | grep -qxF 'claude-review-again'. A failedghinside that pipe read as "Label already removed (benign)", against the step's own comment that every other failure is an error.Fix: here-strings, and the label read captured first.
Driven with a fake
ghthrough the removal step's three branches: present, the edit runs; absent, the benign line;ghfails, exit 1 with the error. Crease commitda14f3f.2. An unset
shell:runsbash -e {0}, with no pipefailGitHub's shell table: unspecified on Linux is
bash -e {0}, and an explicitshell: bashisbash --noprofile --norc -eo pipefail {0}("Note that this runs a different command to when bash is specified explicitly", docs.github.com workflow syntax, read 2026-09-29). Without pipefail,cmd | tee logpasses when cmd fails. The two review templates andtooling.md's CI skeleton set no shell.Fix:
defaults: run: shell: bashin both review templates, the CI skeleton, and scaffold's starter CI stub. Crease commits7f46fa3,86da5e1.3.
runs-on: ubuntu-latestmoves when GitHub re-points the labelA runner-image change then lands in every target unreviewed. It does not break required checks, which match on job name.
Fix:
runs-on: ubuntu-24.04with the reason, in the two review templates, the CI skeleton and the starter stub. Crease commit86da5e1.4. The kit says no bot covers a container base image; Dependabot covers most, with three gaps
§ Dependency settle-window,
tooling.mdsanity question 6b and the starter's "Not covered by any bot" line (which sits right under a commenteddockerecosystem stub) all say a container base-image tag is outside every update bot. For most images it is not. Dependabot'sdockeranddocker-composeecosystems bump a pinnedrepo:tag@sha256tag and digest together, and the docs' cooldown table lists both as supportingdefault-days. The three real gaps, measured in crease-data/crease#276 and checked against dependabot-core's source (read 2026-09-29):FROMlines in a Dockerfile (and Kubernetes and Helm image references in YAML), neverCOPY --from=<image>. Route such an image through a namedFROM … AS <stage>, as crease's fastapi image does for uv;devcontainersecosystem bumps a devcontainer's Features, not itsimage(devcontainers/…/file_parser.rb'sparsecalls onlyparse_features). A digest-pinned devcontainer image is hand-bumped. crease's own crease-data/crease#276 docs claimed otherwise and are now corrected;tag_last_pushedis currently the only accepted source", dependabot-coredocker/README.md, read 2026-09-25). So a GHCR, quay.io or MCR bump gets its release-age check by hand.Fix: all three surfaces say an image in a Dockerfile
FROMor a composeimage:is covered, and name the three gaps. Crease commits712bdd9,0b3177a.5. mise runs inline tasks under
sh -c -o errexit(dash), with no pipefail (template not carried in crease)tooling.mdoffers mise as a task runner. mise's default inline shell issh -c -o errexit, and on Debian/Ubuntushis dash, so there is no pipefail. crease's fix (commitb76adbfon crease-data/crease#276), for the kit's mise template:crease's
tests/test_shell_pipefail.pyfails the build if the shell drops pipefail or a task overrides it.6. Unpinned stdio MCP servers run network-fetched code at every session start (template not carried in crease)
.mcp.json.examplelaunchesnpx -y @modelcontextprotocol/server-github,…-linearand…-timewith no version. That fetches whatever is newest at every session start, outside every lockfile and the settle window. crease pins each server to an exact version past the window (@modelcontextprotocol/server-github@2025.4.8,@upstash/context7-mcp@4.1.1,mcp-server-time@2026.8.18, …).tests/test_supply_chain_pins.py::test_every_stdio_mcp_server_runs_a_pinned_versionfails on an unpinnednpx/uvxserver. Crease commit344a46aon crease-data/crease#276. For the kit: pin the example's servers, and say in the settle-window section that an MCP server is a dependency.Where it lands in the kit
blueprint/references/templates/claude-review.ymlda14f3f,7f46fa3,86da5e1,293f03b(the removal step's comment: a label deleted from the repository is the benign branch)blueprint/references/templates/claude.ymlblueprint/references/templates/tooling.md(CI skeleton; 6b)7f46fa3,86da5e1,712bdd9scaffold/references/github-starter-templates.md(the CI stub; "not covered by any bot")86da5e1,712bdd9cbk-conventions-reference.md§ Dependency settle-window712bdd9.mcp.json.examplecrease's
tests/test_shell_pipefail.pyandtests/test_supply_chain_pins.pynow read the kit templates as well as crease's workflows (commit38fd348, red first). For the kit itself, the same three assertions are a kit sub-block check in crease's verification block (commitd4bf867):Three mutants of the review template (
ubuntu-latest,shell: sh, a label check piped back throughprintf) each turn it red.