Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
75 commits
Select commit Hold shift + click to select a range
2733f25
fix(guard): keep the enclosing command whole across a substitution
REPPL Sep 25, 2026
98091ec
feat(guard): expand an unquoted brace group the way bash does
REPPL Sep 25, 2026
0036237
fix(guard): resolve an alias declared inside a bang alias body
REPPL Sep 25, 2026
e5dcc48
fix(guard): refuse an uncommitted weakening override; decide the load…
REPPL Sep 25, 2026
6b6e5f5
feat(guard): block a kill by name or pattern
REPPL Sep 25, 2026
b7538e8
feat(guard): warn on a bare stash where worktrees share the stack
REPPL Sep 25, 2026
a29b1ca
test(guard): assert the cost bounds as counts of work, not wall-clock…
REPPL Sep 25, 2026
4d73263
chore: capture iss-2609251144159533 — double-quoted substitution not …
REPPL Sep 25, 2026
33a2bb2
fix(guard): follow a command substitution inside double quotes
REPPL Sep 25, 2026
6e95307
chore: defer iss-213 and iss-2608230847432285 out loud to itd-148's o…
REPPL Sep 25, 2026
a8b3e7d
chore: resolve iss-148 and iss-2608221126066631 — substitutions keep …
REPPL Sep 25, 2026
b8b5ae2
chore: resolve iss-2608282026038930 — brace groups are expanded, not …
REPPL Sep 25, 2026
3f7bbaa
chore: resolve iss-2609020348038749 — bang alias bodies re-enter the …
REPPL Sep 25, 2026
8175987
chore: resolve iss-147 and iss-2608291814576261 — committed-only weak…
REPPL Sep 25, 2026
15ddd6d
chore: resolve iss-2609240646538696 — kill by pattern blocks
REPPL Sep 25, 2026
fee1622
chore: resolve iss-2609190338340796 — shared-stash warn
REPPL Sep 25, 2026
59f9673
chore: resolve iss-2609251144159533 — double-quoted substitutions are…
REPPL Sep 25, 2026
1959f94
docs(plans): repoint the iss-147 and iss-148 links at their resolved …
REPPL Sep 25, 2026
7582dfc
chore: capture the review-guard findings on the shell guard
REPPL Sep 25, 2026
d1011df
fix(guard): read quoted and operand-position substitutions fail-closed
REPPL Sep 25, 2026
6542da8
chore: resolve the three in-lane guard findings — substitutions fail …
REPPL Sep 25, 2026
c6b6004
fix(guard): read git long-flag abbreviations as git does
REPPL Sep 25, 2026
0d07884
chore: resolve iss-2609251640354925 — git long-flag abbreviations
REPPL Sep 25, 2026
5527f0e
fix(guard): read pushd and popd as the directory change after_cd means
REPPL Sep 25, 2026
b4da173
chore: resolve iss-2609251640464735 — pushd and popd chain like cd
REPPL Sep 25, 2026
5e05fad
fix(guard): read a core.hooksPath override as skipping the hooks
REPPL Sep 25, 2026
3185491
chore: resolve iss-2609251640464212 — hooks-path override is no-verify
REPPL Sep 25, 2026
7e523ba
fix(guard): read a substitution's output as one unknown word
REPPL Sep 25, 2026
ac56a17
chore: record the guard's unknown-word residuals and re-grade the pip…
REPPL Sep 25, 2026
196476c
chore: resolve iss-2609251640462464 — a shell reading a stream is ref…
REPPL Sep 25, 2026
a9b9571
chore: capture the review3-guard findings on the shell guard
REPPL Sep 25, 2026
83b3de6
fix(guard): end an ANSI-C string at its first NUL, as bash does
REPPL Sep 25, 2026
f38daa9
fix(guard): read every word through the unknown-word rule
REPPL Sep 25, 2026
9c9bcfb
fix(guard): read the stdin device and a process substitution as a stream
REPPL Sep 25, 2026
8a8d158
chore: resolve the three review3-guard findings — the rule is total
REPPL Sep 25, 2026
b5438a1
chore: record the total unknown-word rule and narrow its deferral
REPPL Sep 25, 2026
dc028f4
chore: capture the review4-guard findings on the shell guard
REPPL Sep 25, 2026
b0fc5c6
fix(guard): read the substitutions an unquoted here-document body runs
REPPL Sep 25, 2026
c72a73f
fix(guard): close an ANSI-C string before decoding it, and block a li…
REPPL Sep 25, 2026
f94d60b
fix(guard): read a parameter expansion carrying a substitution as unk…
REPPL Sep 25, 2026
08e3f5e
fix(guard): report a block on a name nothing fixes as the substitution's
REPPL Sep 25, 2026
9124932
test(guard): find a word's dash reader by its mention, not by today's…
REPPL Sep 25, 2026
65e7ef0
docs(guard): say what the guard reads of variables, bodies and ANSI-C…
REPPL Sep 25, 2026
3ce3a2d
chore: record the guard's round-4 rulings and narrow the variable def…
REPPL Sep 25, 2026
0cda7af
chore: resolve the six review4-guard findings — bodies, ANSI-C closes…
REPPL Sep 25, 2026
7533feb
refactor(guard): walk Check's final segments once
REPPL Sep 25, 2026
7266dad
chore: resolve iss-2609252135151652 — Check walks its final segments …
REPPL Sep 25, 2026
7d63acd
chore: capture the review5-guard findings on the shell guard
REPPL Sep 25, 2026
769fd3d
fix(guard): join an unquoted here-document body's backslash-newline b…
REPPL Sep 25, 2026
a0f3bad
chore: resolve iss-2609252214137586 — a here-document body joins its …
REPPL Sep 25, 2026
7b08572
fix(guard): read a double-quoted parameter expansion to its own brace…
REPPL Sep 25, 2026
53bc404
chore: resolve iss-2609252214217550 — a double-quoted parameter expan…
REPPL Sep 25, 2026
14bb635
fix(guard): read a here-document substitution's text as the payload i…
REPPL Sep 25, 2026
cea2b6d
chore: resolve iss-2609252214215409 — a here-document payload is read…
REPPL Sep 25, 2026
cfcafe4
docs(guard): say how the guard reads a body's joined lines, a quoted …
REPPL Sep 25, 2026
51caaf3
chore: record the guard's round-5 rulings and correct the round-4 bod…
REPPL Sep 25, 2026
f97a751
docs(guard): carry the round-5 readings in the guard help the referen…
REPPL Sep 25, 2026
373a3c8
chore: capture the nested and backtick here-document payload gaps
REPPL Sep 25, 2026
7c7dcdf
fix(guard): read a command-position here-document's words at every pa…
REPPL Sep 25, 2026
8c7b1ca
chore: resolve iss-2609252305404421 — nested here-document payloads a…
REPPL Sep 25, 2026
5b72460
fix(guard): read a backtick here-document's output as its dollar spel…
REPPL Sep 25, 2026
403b002
chore: resolve iss-2609252310310823 — backtick here-document output i…
REPPL Sep 25, 2026
e865939
chore: capture the guard's round-7 backtick, IFS, glued-output and re…
REPPL Sep 26, 2026
1d5b5d2
fix(guard): read a backtick's text after bash's backslash pre-pass
REPPL Sep 26, 2026
03abab7
chore: resolve iss-2609260115287911 — a backtick's text is read after…
REPPL Sep 26, 2026
d170310
fix(guard): read a fixed output glued to other text as bash joins it
REPPL Sep 26, 2026
807b4be
chore: resolve iss-2609260115380561 — a glued fixed output is read as…
REPPL Sep 26, 2026
4e64a15
fix(guard): refuse an unquoted fixed output on a line that names IFS
REPPL Sep 26, 2026
97474ce
chore: resolve iss-2609260115387303 — a fixed output on a line naming…
REPPL Sep 26, 2026
8ad5977
docs(guard): name the lone-substitution allow and the exact-shape doc…
REPPL Sep 26, 2026
4d6e3ce
chore: resolve iss-2609260115383631 — the residual lists name the lon…
REPPL Sep 26, 2026
605ac84
refactor(guard): write the backtick pre-pass loop plainly
REPPL Sep 26, 2026
cc24add
docs(guard): name the IFS a line gains unread, and the quote the pre-…
REPPL Sep 26, 2026
d1127c4
Merge branch 'main' into fix/guard-cluster
REPPL Sep 26, 2026
eaf47cf
Merge branch 'main' into fix/guard-cluster
REPPL Sep 26, 2026
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
118 changes: 97 additions & 21 deletions .abcd/development/brief/04-surfaces/17-guard.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,10 +57,13 @@ worth having rather than merely obstructive.
The exit codes are the contract, and the asymmetry in them is deliberate. On the
hook, only exit 2 stops anything; a warn exits 1 because a pre-tool-use hook that
exits 0 has its stderr discarded, so a warn returning 0 would run as if allowed
with nobody told (iss-231). A guard that cannot answer at all — an unparsable
command line, a registry with nothing left to check against, a registry switched
off — exits 1 on the hook and lets the command run, and exits 2 on the check so
that a script never reads silence as clearance.
with nobody told (iss-231). A guard that cannot answer at all — a registry with
nothing left to check against, a registry switched off — exits 1 on the hook and
lets the command run, and exits 2 on the check so that a script never reads
silence as clearance. A command line the guard cannot split is the exception on
the hook: it is blocked (`command-unparsable`), not let through, because a line
the guard misreads may be one bash runs, and a pass would carry every hazard in
it past the guard. On the check it exits 2, like the rest.

Either verb also speaks JSON, and that is the form the plugin page uses: a
verdict, and with it the entry that fired, its tier, why the command is
Expand All @@ -82,7 +85,9 @@ The states that can independently be false are reported outside the session, on
calls is reachable, and whether a hazard registry is armed. A repo
`.abcd/guard.json` that will not load drops the repo's own overrides while the
bundled hazards stay armed, and that middle state is reported as itself rather
than folded into either extreme.
than folded into either extreme. The three states — clean, repo layer dropped,
no registry at all — are decided once, in the core, and every caller formats
the same answer.

The two callers part company on exactly that file, deliberately. **On the hook,
the session keeps its protection:** the repo's overrides are dropped with a
Expand All @@ -106,12 +111,14 @@ running on a registry it cannot trust.

There is no flag, environment variable, or prompt that disarms the guard for a
session. The file is the only route, so switching the guard off lands in a diff
somebody reviews. What is not yet enforced is that the diff is *committed*: the
registry is read from the working tree, so an uncommitted edit takes effect on
the next command. The mitigation today is loudness rather than refusal — a
disabled registry makes every command it lets through carry an `UNGUARDED`
warning naming the file, and `abcd ahoy` reads `OFF`. Refusing a `disabled: true`
that is not in `HEAD` is a core-side change, tracked as an issue.
somebody reviews, and the diff must be *committed* before it counts. An edit
that weakens the registry — switching it off, or changing a blocker's tier or
pattern — is refused until `HEAD` carries it: the committed registry stays in
force, the hook announces the refused edit on every command, and the check
refuses to answer. Where git cannot say what `HEAD` carries, the edit is refused
too. An edit that only adds or tightens a hazard needs no commit. Once a
switch-off is committed, every command it lets through carries an `UNGUARDED`
warning naming the file, and `abcd ahoy` reads `OFF`.

## What this guard is, and is not

Expand Down Expand Up @@ -168,20 +175,89 @@ hazard behind a launcher it does not recognise is a **warn** naming the entry it
matched rather than an allow, because the guard cannot tell whether that program
runs the rest of the line. An unquoted glob is treated as producing whatever
literal it could produce, at every position an entry constrains, so a force push
spelled `git pus? --force` blocks. A command string handed to a shell is opened
and read. A git alias declared on the same command line is resolved, and the
command git would actually run is what gets checked. Where the reading is a
spelled `git pus? --force` blocks. A git long flag written short of its full
name is read as git reads it, as the one option that prefix can mean. A command
or process substitution, unquoted
or inside double quotes, is followed into command position, and the words
written after one stay the
enclosing command's, so `rm $(true) -rf *` is read as `rm -rf *`. What a
substitution prints is not in the line, so a word holding one is unknown and
fails closed in every role it could play, read every way it can be at once: led
by a dash it is every flag it could become — one standing alone, one taking a
value, a shell's `-c` — before the command as well as after it; after a value
flag it is that flag's value; as an operand it is one operand; and in command
position it is any program its known text allows, a shell, a wrapper and git
among them; where the only entries that fire are ones such a name can be, the
block is reported as the substitution's (`program-name-unknown`), and its way
past is to spell the program's name. Every reader of a word goes through that one rule, and a test holds
the package to it. Text beside one in the same word is also read as bash leaves
it when the output is empty. One nested past the depth the guard reads, one
holding a case command, or more of them where the program name could be than
the guard follows, is refused rather than left unread. A parameter expansion
holding a substitution prints its output, so its word is unknown from the `${`
on, and inside double quotes one ends at its own `}`, where a nested `"` opens a
string of its own. A here-document body is data, but the substitutions the shell
runs in a body whose delimiter is unquoted are read as commands, and such a body
is read by the lines bash compares with its delimiter, joined across a trailing
odd run of backslashes. A backtick's text is read after bash's own pass over
it, which drops a backslash before `$`, a backtick or a backslash (and, directly
inside double quotes, a `"`), so an escaped substitution between backticks is
read as the one bash runs. A payload that is wholly a substitution printing a
here-document the shell does not change (`sh -c "$(cat <<'EOF' … EOF)"`, or the
backtick spelling where no backslash stands between the backticks) is also
read as that document's text, joined to any text beside it in the word. The same
substitution unquoted runs the words its document splits into, the first and
last joined to whatever is written against it in its word, and is read as those
words wherever it stands and at every payload layer, so a document whose text is another such substitution is read
too; the words are never read again as a command line, as bash never reads
them. On a line where another command names IFS such an output is refused,
because the guard splits on the default IFS only and does not work out which
assignment reaches which expansion; a prefix assignment, which does not reach
its own command's expansion, is read as the default split. The guard follows two execute-a-string layers and refuses a payload
nested deeper, whatever it holds, because it has stopped reading it. An ANSI-C string ends at its
closing quote, found before any escape is decoded, and at its first NUL, as bash
ends it. An arithmetic expansion is an expression, not commands. A shell reading its script from a pipe, a here-document or a
here-string is refused, because what it runs is text the guard read as data, and
so is one handed the stdin device behind a pipe or a process substitution as its
script, a `source` of one, and a line longer than the guard reads. An unquoted
brace group is expanded as bash expands it and every word it produces is
checked, so `mkdir -p foo/{a,b}` passes and `git push {--force,} origin main`
blocks; a group past the expansion cap is refused rather than read in part. A
command string handed to a shell is opened and read. A git alias declared on the same command line is resolved, and the
command git would actually run is what gets checked. A commit or push that
moves `core.hooksPath` for itself is read as skipping its hooks, which is what
it does. A delete chained after `pushd` or `popd` is read as one chained after
`cd`. In a repository with more
than one worktree, a stash or pop that does not name its entry is warned about,
because the stash stack is shared across worktrees. Where the reading is a
guess, over-blocking is the direction the guard takes.

What an allow still does not see is a hazard that never reaches command position
at all: one behind a wrapper flag the per-wrapper table does not name; a REST
at all: a word that is wholly a command substitution standing where a flag
would be, which is read as an operand because that is how a commit message or a
branch name is spelled every day; one behind a wrapper flag the per-wrapper
table does not name; a REST
path an entry names by its root segment when the host serves that API under a
prefix; a bare `$VAR` standing where the hazard would be inside a payload the
guard does read, because the guard sees the variable and not what the shell will
expand it to, and warning on every variable would bury the warnings that matter;
a payload inside a non-shell interpreter such as `python -c`, which is one
opaque token and today a silent allow; and any dangerous form no entry
describes. The check's own help text is the fuller statement of the same list,
prefix; an IFS the shell already holds when the line starts, or gains during the line
through a name the guard does not read (`declare $(echo I)FS=x`, a sourced file),
since every line is read from the default IFS; a payload inside a non-shell interpreter such as `python -c`, which is
one opaque token and today a silent allow; and any dangerous form no entry
describes. Nor does an allow see through a parameter expansion that carries no
substitution (`$VAR`, `${VAR:-git}`), wherever it stands — as the command's
program name, as a flag, or inside a payload the guard does read — because the
guard sees the variable and not what the shell will expand it to, and warning on
every variable would bury the warnings that matter; so the obvious evasions above
do not include a hazard spelled through a variable. Nor does an allow see what a
lone substitution prints when it stands as the whole command (`$(cat msg.txt)`,
`$(date)`): such a name can be any program, but with no operand after it no
entry matches, so it allows by the posture above, where an allow means no entry
matched. What a substitution prints is read only for the exact shape `cat
<<DELIM`, a newline, the body, the delimiter line and blanks. `/bin/cat`,
`command cat`, `cat -`, a redirection after the delimiter word, a
backslash-newline before the `<<`, a command after the document, a
backslash-newline after a backtick's document, and an output inside a `${…}`
all leave it unknown, so each of those standing alone as the command allows,
though bash runs what it prints; handed to `sh -c` as its string, each warns. The check's own help text is the fuller statement of the same list,
kept beside the code that implements it, with a worked example for each and the
near-misses that *are* read spelled out beside them.

Expand Down
4 changes: 2 additions & 2 deletions .abcd/development/plans/2026-08-15-plugin-user-safety.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,9 +85,9 @@ once. Human-paired (the §4 gate is manual by design).
the backward search. Fix-eligible by the 2026-08-08 ruling (it escaped the
adjacency shelving: a cost bug, not a window-truncation bug).
Autonomous-eligible.
7. **[iss-147](../../work/issues/open/iss-147-guard-load-reads-abcd-guard-json-from-the-working-tree-so-a.md)**
7. **[iss-147](../../work/issues/resolved/iss-147-guard-load-reads-abcd-guard-json-from-the-working-tree-so-a.md)**
(minor) — working-tree guard config is an instant disarm.
8. **[iss-148](../../work/issues/open/iss-148-guard-registry-coverage-gaps-found-while-wiring-itd-103-regi.md)**
8. **[iss-148](../../work/issues/resolved/iss-148-guard-registry-coverage-gaps-found-while-wiring-itd-103-regi.md)**
(minor) — registry coverage gaps; every entry lands fixture-first per the
v0.5.0 plan's rule.
9. **[iss-174](../../work/issues/open/iss-174-rules-override-withholds-bundled-default-upgrades.md)**
Expand Down
Loading
Loading