Skip to content

Commit a5fdec5

Browse files
Ship 1.5.0, and fail when the version stops describing the payload
A packaging audit found the label covering three different trees. Both manifests said 1.4.0 while the installable payload sat nine commits ahead of that tag — including a whole new hook. `VSTACK_REF=v1.4.0` got the tag; the unpinned bootstrap and the plugin marketplace both take the default branch. Three lanes, three trees, one version, and a changelog describing none of what the last two actually delivered. Check 24 is the fix that outlives this release. It does not demand that HEAD always be a release — only that if a tag exists with the version the manifests declare, the installable payload is identical to it. Between releases you bump the version and it goes quiet until that version is tagged. It went red on its first run, which is why 1.5.0 exists. Docs and tests are deliberately outside its scope: they change constantly without changing what a stranger receives. Two smaller things from the same audit. NOTICE still named two MIT upstreams after gstack's notice was vendored as the third — the notice was present, the cross-reference to it was not. And the README described the plugin lane as shipping "the routing hook" when hooks.json declares two: routing, and a Stop gate that stays inert until a repo's verify.sh is trusted. 1.5.0 itself is the second external review's findings: the cloud gate that was installed and never ran, credentials exported into every shell, a guard adapted from gstack and armed by default, a pinned bootstrap that 404'd for tags, six skills pointing at skills this port does not vendor, and a first screen that led with the widest lane instead of the narrowest. verify.sh 26/26, falsifiability 27/27, matrix 19/19, doctor and drift green.
1 parent 2d2d198 commit a5fdec5

7 files changed

Lines changed: 90 additions & 7 deletions

File tree

.claude-plugin/marketplace.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
"plugins": [
1111
{
1212
"name": "vstack",
13-
"version": "1.4.0",
13+
"version": "1.5.0",
1414
"source": "./claude",
1515
"description": "26 skills that fire without a slash command, 8 agents, 14 commands, and the session hook that routes situations to skills. Most skills are ported from pstack and Superpowers — see claude/skills/ATTRIBUTION.md for per-skill source and license.",
1616
"category": "workflow"

.claude/verify.sh

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -881,6 +881,36 @@ else
881881
skip "destructive guard decides correctly" "jq not installed"
882882
fi
883883

884+
# --- 24. the declared version describes what actually installs -------------------------------
885+
# Three lanes install three different trees, and only one of them is pinned. `VSTACK_REF=v1.4.0`
886+
# gets the tag; the unpinned bootstrap and the plugin marketplace both take the default branch.
887+
# So when the payload moves ahead of the tag while the manifests still name it, a stranger
888+
# installs something that is not the version it claims to be — and the changelog describes a
889+
# different artefact than the one they got.
890+
#
891+
# The rule is narrow on purpose: it does not demand that HEAD always be a release. It only
892+
# demands that if a tag exists with the version the manifests declare, the installable payload
893+
# is identical to it. Between releases you simply bump the version, and the check goes quiet
894+
# until that version is tagged.
895+
if command -v git >/dev/null && command -v jq >/dev/null; then
896+
mv_=$(jq -r '.version' claude/.claude-plugin/plugin.json 2>/dev/null)
897+
if [ -z "$mv_" ]; then
898+
bad "declared version matches what installs" "could not read the version from the plugin manifest"
899+
elif ! git rev-parse -q --verify "refs/tags/v$mv_" >/dev/null 2>&1; then
900+
ok "declared version matches what installs (v$mv_ not yet tagged)"
901+
else
902+
# Everything a lane actually delivers. Docs, tests and CI are deliberately excluded: they
903+
# change without changing what a stranger receives.
904+
drift=$(git diff --name-only "v$mv_..HEAD" -- claude/ mcp/ bin/ shell/ conductor/ \
905+
install.sh bootstrap.sh overlay.sh uninstall.sh setup-machine.sh 2>/dev/null)
906+
[ -z "$drift" ] && ok "declared version matches what installs (v$mv_)" \
907+
|| bad "declared version matches what installs" \
908+
"$(printf 'the manifests say v%s but the payload has moved since that tag:\n%s\nbump the version and changelog it, or the plugin and unpinned lanes ship something v%s never described' "$mv_" "$(printf '%s' "$drift" | sed 's/^/ /' | head -10)" "$mv_")"
909+
fi
910+
else
911+
skip "declared version matches what installs" "git or jq not installed"
912+
fi
913+
884914
echo
885915
# Accounting. Every declared check must have reported either a result or a skip. A check
886916
# that throws a shell error mid-body, or is wrapped in a conditional with no else, silently

CHANGELOG.md

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,51 @@ Versions follow [semver](https://semver.org). The version lives in two manifests
66

77
## Unreleased
88

9+
## 1.5.0 — 2026-08-20
10+
11+
A second external review, of the improvements rather than the bugs. Its headline finding was
12+
that the cloud lane's central promise was inert, and it was right.
13+
14+
**The Stop gate never ran in a cloud sandbox.** `overlay.sh` installs `.claude/verify.sh` and
15+
wires the hook, but `verify-gate.sh` refuses to execute a repo's gate without a machine-local
16+
trust entry — and a fresh sandbox has none. Installed, wired, silently skipping on every Stop,
17+
in the one lane that exists for cloud work. The sandbox setup line now runs `vstack trust`.
18+
Local protection is unchanged: an untrusted repo cloned to your laptop still runs nothing until
19+
you type it yourself.
20+
21+
**Credentials are no longer exported into your shells.** `install.sh` sourced `secrets.env` into
22+
`.zshenv` with `set -a`, and the bash lane extended that to `.bashrc` and `.profile`. One token
23+
reached every child process of every shell: every script in every repo, every package
24+
postinstall. Every wrapper in `bin/` already loads what it needs. The installer now removes the
25+
line it previously wrote, and leaves any other spelling alone.
26+
27+
**A destructive-command guard, armed by default.** vstack had no pre-execution interception at
28+
all, which is a strange gap for a setup that recommends `--bypass-permissions` — bypassing
29+
permissions is exactly what removes the prompt that would catch `rm -rf /`. Adapted from
30+
gstack's `careful` skill (MIT, Garry Tan) with two differences: it is always armed rather than
31+
opt-in per session, and its decisions are tested. Sixteen commands across deny, ask and allow,
32+
plus a stripped-environment run.
33+
34+
**The pinned bootstrap worked for branches and not tags**`archive/refs/heads/$REF` 404s for
35+
`VSTACK_REF=v1.4.0`, the exact value the README tells people to pin. And a no-git install could
36+
not be converted once git returned, which was the recovery that path itself recommends. It now
37+
marks its own tarball installs, converts them by moving aside rather than deleting, and refuses
38+
outright to touch a directory it did not create.
39+
40+
**Skills stopped pointing at skills that are not here.** Six `superpowers:` references across
41+
three skills instructed the model to invoke tooling this port does not vendor. Check 7 could not
42+
see them — its token pattern skips the namespace prefix.
43+
44+
**First contact leads with the narrow thing.** The plugin lane is the headline now, with an
45+
invitation to stop after thirty seconds. The workstation setup follows, labelled as one person's
46+
whole environment, with the pinned and readable path before the unpinned one-liner. The plugin
47+
lane also stopped mandating a four-skill chain for every change, which was operating policy in
48+
the one profile whose job is stripping operating policy.
49+
50+
**New checks.** 22 extended to cover skills as well as scripts. 23 tests the guard's decisions.
51+
24 fails when the payload moves ahead of the version the manifests declare — which it caught on
52+
its first run, and which is why this release exists.
53+
954
## 1.4.0 — 2026-08-20
1055

1156
An external adversarial audit of 1.3.0 by a different model returned twelve findings and a

NOTICE

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ Adapted for Claude Code as the `agent-browser` skill.
1515

1616
MIT-licensed work is vendored here too, and MIT requires its copyright and
1717
permission notices travel with it. Those are in LICENSE.mit-upstream
18-
(obra/superpowers and ehmo/platform-design-skills) and in
18+
(obra/superpowers, ehmo/platform-design-skills and garrytan/gstack) and in
1919
claude/skills/LICENSE.pstack (pstack).
2020

2121
Modifications: every vendored skill was adapted for Claude Code — model names

README.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,8 @@ honest version of "it works".
3030
## Start here: the skills alone
3131

3232
If you want better Claude Code behaviour and nothing else, this is the whole thing. It adds
33-
skills, subagents, commands and the routing hook. It does not touch your shell, your
33+
skills, subagents, commands and the routing hook, plus a Stop-hook gate that stays inert until
34+
you trust a repo's own `.claude/verify.sh`. It does not touch your shell, your
3435
credentials, your `~/.claude/settings.json`, or anything else on the machine.
3536

3637
```bash
@@ -102,8 +103,9 @@ nothing — no home directory config of any kind. If the skills are not committe
102103
that session does not have them. `bin/doctor` fails when an active repo has no overlay, and it
103104
finds those repos through their Conductor workspaces rather than a hardcoded list of paths.
104105

105-
The plugin lane is deliberately the narrowest. It ships the skills, subagents, commands and the
106-
routing hook, and stops there. The token, delegation and autonomy rules in the other two lanes
106+
The plugin lane is deliberately the narrowest. It ships the skills, subagents, commands, the
107+
routing hook, and an opt-in Stop-hook verify gate that does nothing until a repo's
108+
`.claude/verify.sh` is trusted — two hooks, not one. The token, delegation and autonomy rules in the other two lanes
107109
are one person's operating policy and have no business arriving with a skill pack a stranger
108110
installed.
109111

claude/.claude-plugin/plugin.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "vstack",
3-
"version": "1.4.0",
3+
"version": "1.5.0",
44
"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.",
55
"author": {
66
"name": "Vedant Kumar"

tests/gate-falsifiability.sh

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ set -uo pipefail
1818
cd "$(dirname "${BASH_SOURCE[0]}")/.." || exit 1
1919

2020
# One id per `# --- N.` section in .claude/verify.sh. Check 16 parses this line.
21-
CHECKS="0 1 2 3 4 5 6 7 8 9 9b 10 11 12 13 14 15 16 17 18 19 20 21 22 23"
21+
CHECKS="0 1 2 3 4 5 6 7 8 9 9b 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24"
2222

2323
BK=$(mktemp -d)
2424
NOJQ=$(mktemp -d)
@@ -67,6 +67,7 @@ files_for(){ case "$1" in
6767
21) printf 'install.sh' ;;
6868
22) printf 'claude/skills/swarm/SKILL.md' ;;
6969
23) printf 'claude/hooks/guard-destructive.sh' ;;
70+
24) printf 'claude/.claude-plugin/plugin.json' ;;
7071
esac }
7172

7273
# The label the gate must print. Matched against the FAIL lines only.
@@ -96,6 +97,7 @@ label_for(){ case "$1" in
9697
21) printf 'RETIRED names only retired keys' ;;
9798
22) printf 'skills disclose what they do not ship' ;;
9899
23) printf 'destructive guard decides correctly' ;;
100+
24) printf 'declared version matches what installs' ;;
99101
esac }
100102

101103
# Break exactly what the check watches, and nothing else. Surgical matters: a mutation that
@@ -167,6 +169,10 @@ exit 0
167169
# and the ask/allow tiers keep working, so only a test of the decisions notices.
168170
sed -i.t 's/^if \[ "\$SIMPLE" = 1 \]; then/if false; then/' claude/hooks/guard-destructive.sh \
169171
&& rm -f claude/hooks/guard-destructive.sh.t ;;
172+
24) # claim an already-tagged version while the payload has moved on — the exact state that
173+
# ships three lanes three different trees under one label
174+
sed -i.t 's/"version": "[0-9.]*"/"version": "1.4.0"/' claude/.claude-plugin/plugin.json \
175+
&& rm -f claude/.claude-plugin/plugin.json.t ;;
170176
esac }
171177

172178
echo "falsifying $(printf '%s' "$CHECKS" | wc -w | tr -d ' ') checks"

0 commit comments

Comments
 (0)