Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .claude-plugin/marketplace.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
"plugins": [
{
"name": "vstack",
"version": "1.66.0",
"version": "1.67.0",
"source": "./claude",
"description": "28 skills that fire without a slash command, 14 agents, 15 commands, and the session hook that routes situations to skills. Most skills are ported from pstack and Superpowers \u2014 see claude/skills/ATTRIBUTION.md for per-skill source and license.",
"category": "workflow"
Expand Down
131 changes: 131 additions & 0 deletions .claude/verify.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2739,6 +2739,14 @@ p_errs=""
# placeholder paths (claude/skills|agents|commands), third-party evidence (docs/provenance|research),
# installed artifacts (.claude/), historical entries (CHANGELOG), and third-party test results
# (tests/evals/RESULTS.md). This matches check 12's approach and keeps the two consistent.
# Destinations `vstack overlay .` SEEDS into a TARGET repository, derived from overlay.sh rather
# than listed. README's overlay table names `.github/workflows/security.yml` in its "Lands at"
# column: that is a path in somebody else's repo after the overlay runs, the same category as the
# `.claude/*` exemption below, and holding this tree to it would demand vstack ship every file it
# hands to other people. Derived, so retiring a template retires its exemption with it -- a
# hardcoded list here would outlive the thing it excuses, which is the failure check 20d exists
# for. Leading and trailing spaces so the membership test below cannot match a prefix.
p_seeded=" $(grep -oE '^seed_tmpl [^ ]+ [^ ]+' overlay.sh 2>/dev/null | awk '{print $3}' | tr '\n' ' ')"
p_docs=$(git ls-files '*.md' 2>/dev/null | grep -vE '^(docs/(provenance|research)/|claude/(skills|agents|commands)/|\.claude/|CHANGELOG\.md$|tests/evals/RESULTS\.md$)')
p_n=$(printf '%s' "$p_docs" | grep -c .)
if [ "$p_n" -lt 8 ]; then
Expand All @@ -2758,6 +2766,7 @@ else
# this one. Paths this repository does ship live under claude/, without the dot.
.claude/*) continue ;;
esac
case "$p_seeded" in *" $ref "*) continue ;; esac
p_seen=$((p_seen + 1))
[ -e "$ref" ] && continue
[ -e "${d%/*}/$ref" ] && continue
Expand Down Expand Up @@ -4667,6 +4676,128 @@ else
fi
fi

# --- 64. the security lane skips a missing scanner and still fails on a real finding -----------
# The security toolchain shipped as three payload files and a README table, and every part of it
# was prose. Nothing ran claude/security-scan.sh, nothing read the workflow template's action
# pins, and nothing compared the tools README's "Prod-ready gates" table publishes against the
# tools the script actually calls. The failure mode is not that the scan is missing -- it is that
# the scan runs, finds no scanners installed, prints five reassuring lines and exits 0. That is a
# fake green with a per-tool report attached, which reads stronger than no check at all.
#
# Measured in a throwaway git repo, never in this one: the answer here is contaminated by whichever
# scanners this particular machine happens to have installed, so a run on the author's laptop and
# a run in CI would be testing two different scripts.
#
# Lane 1 -- PATH=/usr/bin:/bin, so no scanner is reachable. Every tool must SKIP and the script
# must exit 0. A missing scanner failing the run would make the overlay's gate red on every
# machine that has not installed five binaries, which is how a gate gets switched off.
# Lane 2 -- the same repo with a stub `gitleaks` that reports a finding and exits 1. The script
# must exit 1 and name it. Lane 1 alone is satisfied by a script that can only ever skip, which
# is precisely the shape lane 1 cannot distinguish from a working scan.
# Lane 64b -- the half that is not executable here: the CI workflow's pins and the README table
# that documents which tool runs where.
c64_lbl="the security lane skips a missing scanner and fails on a finding"
c64_sh="claude/security-scan.sh"
c64_wf="claude/security.yml.tmpl"
if [ ! -f "$c64_sh" ]; then
bad "$c64_lbl" "$c64_sh is missing, so overlay.sh ships a verify.sh template that calls a script nobody wrote"
elif [ ! -f "$c64_wf" ]; then
bad "$c64_lbl" "$c64_wf is missing, so the CI half of the lane README publishes does not exist"
else
c64_errs=""
c64_note=""

# The exec bit as GIT records it, not as this filesystem shows it. A 100644 payload script is
# runnable here (the author chmod'd it once) and unrunnable for everyone who clones, and the
# overlay's own chmod hides that from the only lane that would have caught it.
c64_mode=$(git ls-files -s "$c64_sh" 2>/dev/null | awk '{print $1}')
[ "$c64_mode" = "100755" ] \
|| c64_errs="$c64_errs\n$c64_sh is ${c64_mode:-not tracked by git}, not mode 100755; a stranger's clone gets a file it cannot execute"

# Skip, not fail, on a host without shellcheck -- check 0 already decides whether this gate may
# run at all without it, and duplicating that judgement here would fail hosts check 0 passes.
if command -v shellcheck >/dev/null 2>&1; then
if ! c64_sc=$(shellcheck -S warning -f gcc "$c64_sh" 2>/dev/null); then
c64_errs="$c64_errs\nshellcheck -S warning on $c64_sh: $(printf '%s' "$c64_sc" | head -5 | tr '\n' ';')"
fi
else
c64_note=", shellcheck not on PATH"
fi

c64_tmp=$(mktemp -d "${TMPDIR:-/tmp}/vstack-c64.XXXXXX")
git -C "$c64_tmp" init -q >/dev/null 2>&1
cp "$c64_sh" "$c64_tmp/security-scan.sh"
chmod 755 "$c64_tmp/security-scan.sh"

# env -i: this shell's PATH carries whatever the developer has installed, and inheriting it is
# the difference between measuring the script and measuring the machine.
c64_out=$(cd "$c64_tmp" && env -i PATH=/usr/bin:/bin HOME="$c64_tmp" TMPDIR="$c64_tmp" \
bash ./security-scan.sh 2>&1); c64_rc=$?
c64_nskip=$(printf '%s\n' "$c64_out" | grep -c '^skip' || true)
[ "$c64_rc" -eq 0 ] \
|| c64_errs="$c64_errs\n$c64_sh exited $c64_rc with no scanner on PATH; a tool that is not installed is a skip, and failing there switches the whole gate off for everyone who has not installed five binaries"
[ "${c64_nskip:-0}" -ge 4 ] \
|| c64_errs="$c64_errs\n$c64_sh reported ${c64_nskip:-0} skip line(s) with no scanner on PATH; it declares five tools, and exit 0 over a report that named none of them is the fake green this check exists for"

# Lane 2. The stub is what makes lane 1 mean anything: without it, a script whose every branch
# is `skip` passes lane 1 perfectly.
c64_stub="$c64_tmp/stub"
mkdir -p "$c64_stub"
printf '#!/bin/sh\necho "leak: generic-api-key at config.yml:3"\nexit 1\n' > "$c64_stub/gitleaks"
chmod 755 "$c64_stub/gitleaks"
c64_out2=$(cd "$c64_tmp" && env -i PATH="$c64_stub:/usr/bin:/bin" HOME="$c64_tmp" TMPDIR="$c64_tmp" \
bash ./security-scan.sh 2>&1); c64_rc2=$?
rm -rf "$c64_tmp"
[ "$c64_rc2" -eq 1 ] \
|| c64_errs="$c64_errs\n$c64_sh exited $c64_rc2 against a gitleaks that reported a finding and exited 1; a scanner whose findings do not reach the exit code is decoration"
printf '%s\n' "$c64_out2" | grep -qE '^FAIL +gitleaks' \
|| c64_errs="$c64_errs\n$c64_sh did not print a FAIL line naming gitleaks against a scanner that found something: $(printf '%s' "$c64_out2" | tail -3 | tr '\n' ';')"

# --- 64b, in the same body: the CI half, which cannot be executed here.
#
# Every action pinned to a 40-hex commit with the human-readable tag beside it. A tag is a
# mutable pointer somebody else controls, and `uses: foo@v7` in a workflow with repo write
# gives that somebody a push into this repo's CI. The `# v` suffix is required too: a bare SHA
# with no tag comment is unreviewable and never gets bumped.
while IFS= read -r c64_u; do
[ -n "$c64_u" ] || continue
printf '%s' "$c64_u" | grep -qE '@[0-9a-f]{40} # v' \
|| c64_errs="$c64_errs\n$c64_wf: $(printf '%s' "$c64_u" | sed 's/^[[:space:]]*//') is not pinned to a 40-hex commit with its tag beside it"
done <<EOF
$(grep -E '^[[:space:]]*uses:' "$c64_wf" 2>/dev/null)
EOF
grep -qE '^[[:space:]]*uses:' "$c64_wf" 2>/dev/null \
|| c64_errs="$c64_errs\n$c64_wf declares no 'uses:' line at all, so the pinning rule above scanned nothing"

# README's "Prod-ready gates" table is the published contract; $c64_sh is the implementation.
# The Tool column is extracted rather than typed here, so deleting a row is as visible as
# deleting the tool. Which of those tools the LOCAL gate is expected to run is named, not
# inferred -- npm audit and nuclei are CI and post-deploy only, and inferring the list from
# what the script mentions would let the check read its answer off its own subject.
c64_local="gitleaks semgrep osv-scanner zizmor eslint"
c64_tools=$(awk '/^## Prod-ready gates/{s=1; next} s && /^## /{s=0} s && /^\|/{print}' README.md 2>/dev/null \
| sed -E 's/^\| *//; s/ *\|.*//')
for c64_t in $c64_local; do
grep -qxF "$c64_t" <<<"$c64_tools" \
|| c64_errs="$c64_errs\nREADME's Prod-ready gates table no longer names '$c64_t' in its Tool column, so the published contract and $c64_sh have drifted apart"
grep -qF "$c64_t" "$c64_sh" \
|| c64_errs="$c64_errs\n$c64_sh never mentions '$c64_t', which README's Prod-ready gates table says the local gate runs"
done

# A payload file the overlay does not copy ships to nobody. All three landed in this repo
# before overlay.sh knew about any of them.
for c64_p in claude/security-scan.sh claude/security.yml.tmpl claude/dependabot.yml.tmpl; do
grep -qF "$c64_p" overlay.sh 2>/dev/null \
|| c64_errs="$c64_errs\noverlay.sh never names $c64_p, so it is payload this repository ships to itself"
done

if [ -z "$c64_errs" ]; then
ok "$c64_lbl (${c64_nskip} skips at exit 0, stub finding caught at exit 1, $(grep -cE '^[[:space:]]*uses:' "$c64_wf") actions pinned$c64_note)"
else
bad "$c64_lbl" "$(printf '%b' "$c64_errs")"
fi
fi

# Accounting. Every declared check must have reported either a result or a skip. A check
# that throws a shell error mid-body, or is wrapped in a conditional with no else, silently
# reports nothing — and used to leave no trace in the output at all. Now it fails the run.
Expand Down
18 changes: 18 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,24 @@ Versions follow [semver](https://semver.org). The version lives in two manifests
`.claude-plugin/marketplace.json` and `claude/.claude-plugin/plugin.json`, and check 13 of
`.claude/verify.sh` fails when they disagree.

## 1.67.0 — 2026-09-02

- **A security toolchain ships with the overlay.** `claude/security-scan.sh` runs gitleaks,
semgrep, osv-scanner, zizmor and eslint locally and is copied into every overlaid repo, where
the `verify.sh` template calls it; a scanner that is not installed skips rather than failing, so
the gate does not go red on a machine missing five binaries. `claude/security.yml.tmpl` and
`claude/dependabot.yml.tmpl` are seeded once into `.github/workflows/security.yml` and
`.github/dependabot.yml` and never overwritten — a second `vstack overlay .` reports whether the
file still matches the template or has drifted from it, and names the file to diff against.
`setup-machine.sh` installs the scanners by default.
- **Check 64 measures it in both directions.** The scan is run in a throwaway repo with
`PATH=/usr/bin:/bin` (every tool must skip, exit 0) and again against a stub gitleaks that
reports a finding (must exit 1 naming it), because a scan that can only ever skip passes the
first lane perfectly. 64b requires every `uses:` in the workflow template to be pinned to a
40-hex commit with its tag beside it, and requires README's Prod-ready gates table and the
script to name the same tools. Falsifiability rows 64 and 64b delete the skip branch and unpin
one action.

## 1.66.0 — 2026-09-02

Auto-enforcement tightened after measuring it. Across ten real sessions in the last ten days the
Expand Down
38 changes: 34 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ Two directory pairs differ only by a leading dot, and the difference is the whol
| path | what it is |
|---|---|
| `claude/` | the **shipped payload** — skills, subagents, commands, hooks, installed to `~/.claude/` |
| `.claude/verify.sh` | **this repository's own gate**, 64 checks; not shipped to anyone |
| `.claude/verify.sh` | **this repository's own gate**, 65 checks; not shipped to anyone |
| `conductor/` | payload copied to `~/.conductor/` |
| `.conductor/` | this repository's own workspace config |
| `tests/` | the suites: the falsifiability harness, the install matrix, trigger and baseline tests |
Expand Down Expand Up @@ -192,6 +192,15 @@ normally, but hooks, the CLI wrappers, the shell lane and MCP servers only land
install. Take that lane if you want the Stop-hook gate, the destructive-command guard, or the
CLI wrappers under `~/.config/agents/bin/`.

**`vstack overlay .`** (a third lane — drops config into a target *repo*, not `~/.claude`; the
only lane a cloud sandbox without your home directory can reach):

| Source | Lands at | Condition |
|---|---|---|
| `claude/security-scan.sh` | `.claude/security-scan.sh` | always overwritten |
| `claude/security.yml.tmpl` | `.github/workflows/security.yml` | seeded if absent |
| `claude/dependabot.yml.tmpl` | `.github/dependabot.yml` | seeded if absent |

## Day to day

| Command | What it does |
Expand All @@ -209,7 +218,7 @@ reaches for `unslop`, reviewing TypeScript reaches for `typescript-best-practice

## Checks that can fail

The gate is 64 checks (this number moves as checks are added; check 12 fails if this prose
The gate is 65 checks (this number moves as checks are added; check 12 fails if this prose
and the tree disagree, so it stays honest by construction rather than by discipline).
`tests/gate-falsifiability.sh` breaks the repository once per check, at
least once and more where a check can fail in more than one way, requires the gate to go red
Expand All @@ -218,13 +227,13 @@ naming that check, restores the tree byte for byte, and fails if anything was le
can fail.

```bash
./.claude/verify.sh # 64 checks
./.claude/verify.sh # 65 checks
VSTACK_FALSIFY_ROWS=27 ./tests/gate-falsifiability.sh # one row
git clone . /tmp/vstack-check && cd /tmp/vstack-check && ./tests/gate-falsifiability.sh
```

The full sweep runs the whole gate once per mutation, so the cost is O(rows x checks).
At 108 falsifiability rows and a ~84s gate, that is over two hours serially. This paragraph claimed
At 110 falsifiability rows and a ~84s gate, that is over two hours serially. This paragraph claimed
twenty minutes for four releases, which was the sharded figure wearing the serial one's label.
`./tests/falsify-parallel.sh` runs the same sweep across seven isolated clones. That is the split
CI uses, and CI finishes it in about 19 minutes because its seven shards are seven machines.
Expand Down Expand Up @@ -300,6 +309,27 @@ Stated limits, none of which this fix closes:
check, a human — which is outside a hook's reach. The gate is a guard against finishing on a red
tree by accident, not against an adversary sharing your uid.

## Prod-ready gates

Mandatory for any repo that is public or serves prod traffic. `vstack overlay .` installs the
local gate and CI; `/security` runs the post-deploy steps.

| Tool | Local gate | CI | Post-deploy | Command |
|---|---|---|---|---|
| gitleaks | tracked files (+ `--staged` mode) | SHA-pinned action, `fetch-depth: 0` | history sweep | `gitleaks git --log-opts="--all" --no-banner --redact .` |
| semgrep | `p/typescript` `p/nextjs` `p/owasp-top-ten` | `semgrep ci` | | `semgrep scan --config p/owasp-top-ten .` |
| osv-scanner | `--lockfile` | `google/osv-scanner-action` | | `osv-scanner scan source --lockfile <lockfile>` |
| zizmor | when `.github/workflows` exists | own job | | `zizmor --min-severity medium .github/workflows` |
| eslint | when config exists | `ci.yml` lint step | | `eslint .` |
| npm audit | | `--audit-level=high` when lockfile | | `npm audit --audit-level=high` |
| nuclei | | | `-u <preview-url> -severity medium,high,critical` | `nuclei -u <url> -severity medium,high,critical -silent` |
| OWASP ZAP baseline | | | docker | `docker run --rm -t zaproxy/zap-stable zap-baseline.py -t <url>` |
| trivy | | | only if repo ships a container | `trivy fs --scanners vuln,secret,misconfig .` |

Skipped on purpose: Snyk and SonarQube (commercial, redundant with the table above), husky and
lefthook (vstack gates via the Claude Stop hook, not git hooks — see
[What the hooks decide](#what-the-hooks-decide)).

## The team

Fourteen subagents, each with its own context window and its own tool allowlist, dispatched by the
Expand Down
2 changes: 1 addition & 1 deletion claude/.claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "vstack",
"version": "1.66.0",
"version": "1.67.0",
"description": "Skills that fire on the situation instead of a slash command, plus the subagents, commands, and session hook that make them fire. Verification gates, parallel fan-out, code review, and writing discipline.",
"author": {
"name": "Vedant Kumar"
Expand Down
Loading
Loading