Skip to content

Setlist 1.0.7 (edition v1.6)

Choose a tag to compare

@AlexCiortan AlexCiortan released this 28 Jul 10:25
· 7 commits to main since this release

If you are on 1.0.6, take this one. Four ways past the gates are closed, and
every one of them fires on something a person or an agent types without
meaning anything by it.

A wrapper flag that takes a separate value no longer strands it. 1.0.6
taught both gates to look past a wrapper like nice or env. It stripped the
wrapper word and then stripped flags one word at a time, which is right for a
flag that carries its own value (stdbuf -o0) and wrong for one that does
not. In nice -n 5 git merge --no-ff spec/0001-x, the 5 was left sitting at
the front of the command, the gate stopped recognising the line as starting
with git, and it was never judged at all. env -u VAR git merge ... went the
same way. A flag is now consumed together with the value that follows it, and
the command word itself is never swallowed, so env -i git merge ... keeps
its git.

Discarding working-tree changes no longer looks like switching branches.
git checkout is two commands sharing one name. With a branch it switches;
with a path it throws away local edits and switches nothing. The gate recorded
the argument as a branch either way, so git checkout -- . && git merge --no-ff spec/0001-x concluded the merge would run on a branch named ., decided that
was not your trunk, and skipped every check. Discarding changes right before a
merge is ordinary, so nobody had to be trying. The gate now tells the two
apart: a real branch is tracked, an existing path leaves the branch alone,
anything after -- is a path by definition, and an argument that is neither
refuses rather than guessing. git restore was never affected, and
git switch needs none of this because switch only ever takes a branch.

A bare commit name is refused whatever its shape. Merging a spec branch by
its commit id rather than its name leaves the gate nothing to check, because
the close conditions live in the branch's spec file. It knew that, and
recognised commit ids by their SHAPE: 7 to 40 lowercase hex characters. Every
spelling outside that shape went through instead, including a 6-character
abbreviation and the same id uppercased. Shape was the wrong question. The
gate now asks git whether the word names anything at all, which is free of
shape and also stops a branch that happens to be spelled in hex from being
mistaken for a commit id.

Two spellings of the same file now get the same answer. The scope hook,
which keeps feature code off your trunk, compared paths as text. So
src/app.js was denied while docs/../src/app.js and a path through a
symlinked source directory were allowed onto the trunk in silence. Neither
needs an attacker: .. is what a tool composing paths produces, and symlinked
source directories are ordinary. Paths are now normalised, and where the
filesystem can resolve them they are resolved, with the stricter of the two
readings deciding.

A skill that never loaded now loads. design-surface shipped from 1.0.0
through 1.0.6 with an unquoted colon in its description, which meant its
frontmatter did not parse and the skill loaded with all of its metadata
silently dropped. The release gate that should have caught it was pointed at
the marketplace manifest, which does not read skills at all. The gate now
checks both manifests, and the test suite independently checks that every
shipped skill's frontmatter parses, so this cannot depend on which argument
someone passed to a CLI.

Known limitations gains one entry, which is not a new gap but a newly
described one: the commit gate scans the index of the project it governs, so a
commit aimed at a nested repository or at a named index file is not scanned.
Your own staged content is checked every time. Following the target index
would mean re-deriving which repository each command line means, in every
spelling, and that kind of parser-chasing is what produced two of the holes
this release closes.

Staging is more than git add. The commit gate refuses a command that
writes the index and commits in one step, because it decides before the command
runs and would otherwise scan an index that does not hold your content yet. It
recognised add, rm and mv, so everything else that writes the index went
straight through: git stash pop && git commit -m x and
git restore --staged . && git commit -m x both committed content nothing had
scanned. git stage, a plain synonym for add, was missing for the same
reason. The full set is recognised now, and it is a generated corpus dimension
checked against the gate's own list, so the next missing verb fails the suite
instead of waiting for a reviewer.

A single & separates commands. The gates split a line on &&, ||, ;,
| and newlines, but not on a lone &, which backgrounds what came before it
and starts something new. echo hi & git merge --no-ff spec/0001-x was read as
one command beginning with echo, and the merge was never judged. && is
still one separator rather than two, and an & inside a redirection like
2>&1 still is not one.

A pathspec checkout is not a branch switch, even with a branch in front of
it.
This release already handled git checkout -- .. It did not handle
git checkout other-branch -- src/file, which also restores files and switches
nothing: the gate recorded a switch that never happens, then judged the merge
that followed against the wrong branch. That is the third spelling of one class
to be closed one spelling at a time, so the test is now the -- separator
itself, which is what git uses to tell its own two commands apart.

A closed spec's number cannot be reused to carry unreviewed work. Every
close check reads the spec file as it stands on the branch being merged. That
settles what the artifacts say and not who wrote them. A branch cut from the
trunk after spec 0001 closed inherits that spec, Closing report and all, so a
branch named spec/0001-anything could merge arbitrary changes with no
artifacts of its own and pass every check, and the trunk audit would then call
it compliant because it reads the same inherited artifacts. The gate now
requires the branch to have modified its own spec file. Writing the Closing
report into the spec is what closing a spec is, so an honest close is
unaffected, including the common case of planning a spec on the trunk and
closing it on the branch.

Upgrades check that the gates are actually wired. refresh-instance.sh
verified your hook FILES were current and, separately, that whatever entries
were present in .claude/settings.json were well formed. Nothing checked the
gates were present at all, so an instance with both gate entries deleted was
reported as a complete refresh at exit 0, with a note that the refreshed gates
would bind from the next session. They would never bind again. The same check
also claimed hooks it does not own: a hook of yours living in .claude/hooks/
with no timeout was reported as a Setlist entry, producing an upgrade that
could not be completed because the fix it demanded was editing your own file.
Ownership is by name now, and both directions are covered by the upgrade leg.

Both new bypass classes are now generated corpus dimensions in the test suite
and in the release-time strictness check rather than example cases, so they
stay closed. The suite is at 259 assertions.

Known open issues in this release

An adversarial review ran against the exact tree being published here and found
nine ways past the gates. They are listed rather than quietly carried, because a
backstop you trust more than it deserves is worse than one you know the shape of.

All nine are present in v1.0.6 as well. None is a regression, and every gap
this release closes is closed on top of them, so 1.0.7 is strictly better than
what it replaces. They are queued for 1.0.8; the fix for the largest group is
already designed.

  • Quoted text can hide or forge a git operation. The close gate keeps the
    contents of quoted strings and the commit gate deletes quoted spans, and each
    behaviour is wrong for the other gate. A commit message that merely mentions a
    branch can retarget the close gate; quoting the word git or commit can hide
    a commit from the commit gate; an odd number of quote characters in ordinary
    prose can swallow the rest of the line. Four of the nine are this one defect.
  • A ref spelled an unusual way may not be recognised. The gate normalises a
    handful of prefixes by hand instead of asking git to resolve the ref, so
    heads/spec/0001-x is not seen as the spec branch it is. Related: for a
    remote-tracking ref the gate checks your LOCAL branch of that name, which is
    not necessarily what git is about to merge.
  • Shell compound syntax hides the command. Wrapping a merge in { ...; },
    if ... then, for ... do, or prefixing it with ! moves the git verb out of
    the position the gate looks at.
  • A malformed .claude/sdd.json disables the trunk rule. The parse check
    accepts any valid JSON, including a top-level array or two documents in one
    file, and the hooks then read no trunk and allow everything. A file written by
    hand or half-merged can land in that state.
  • The upgrade refresh does not really verify your gates are wired. New in
    1.0.7 and it does not work: it matches the hook filename as a string anywhere
    in your settings, so a disarmed instance can still report a complete refresh.
    It is an inert check rather than a harmful one (v1.0.6 had no such check at
    all), but do not rely on it. Check .claude/settings.json by eye after an
    upgrade: the scope hook on Write|Edit|MultiEdit|NotebookEdit, and the commit
    and close gates on Bash, all under PreToolUse.

The standing framing has not changed and is worth repeating: these hooks are a
backstop against ordinary mistakes, not a sandbox. Anything that can run a shell
can reach the trunk, and the gates have never claimed otherwise.

If you already have a project running Setlist

Run /setlist:upgrade once after updating the plugin. Nothing in this release
changes the settings wiring, so the refresh is hook files only.

/plugin marketplace update setlist

If a Claude Code session was open when you ran that update, restart it before
upgrading.