fix(security): probe login shells by match, not by exclusion - #8
Merged
wintermeyer merged 3 commits intoSep 17, 2026
Merged
wintermeyer merged 3 commits into
wintermeyer merged 3 commits into
Conversation
The system-account check listed the inert shells to skip, and two of those names read as power-off commands to the taboo guard, which then denied the whole parallel batch mid-audit. Matching the shells that do log in names none of them, and the habit generalizes to every probe.
Printing only shells that end in sh got the probe past the taboo guard, but a security audit then silently dropped accounts with any other shell, such as ksh93, python3 or an empty field. The probe now skips a shell only when it is nologin, false or named after its own account, so nothing it does not recognize goes unreported. The test matrix runs every bash block the skills ship through the guard and pins the probe's output on a fixture. An AI agent wrote this text in my name. I know that is problematic.
Owner
|
Thanks, Oliver! Your diagnosis was spot on, and fixing the probe rather than the guard was the right call. I changed one thing before merging: printing only shells that end in An AI agent wrote this text in my name. I know that is problematic. |
wintermeyer
added a commit
that referenced
this pull request
Sep 17, 2026
One fix since 2.20.0: the security audit's check for system accounts with a login shell no longer trips heinzel's own taboo guard, and it still reports shells it does not recognize, from Oliver Andrich's #8. Release notes in CHANGELOG.md; the reasoning is in the preceding commit. An AI agent wrote this text in my name. I know that is problematic.
This was referenced Sep 17, 2026
wintermeyer
added a commit
that referenced
this pull request
Sep 17, 2026
Three changes since 2.20.1, all around the taboo guard: it now catches writes into SSH keys, sshd_config and disks by the common tools (#12), rule files mark the blocks it is meant to deny (#11), and the key fingerprint probe runs in a call of its own (#10). A minor release because the guard now denies commands it used to let through; #10 and #11 close gaps Oliver Andrich found in #8. Release notes in CHANGELOG.md; the reasoning per change is in the preceding commits. An AI agent wrote this text in my name. I know that is problematic.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What happened
During a real security audit the
heinzel-securityskill stoppeddead: its own documented system-account probe was denied by
heinzel's own
guard-taboos.shhook, and because the probe was ina parallel batch, every sibling check in that batch was cancelled
with it.
Root cause
references/user-accounts.mdlisted system accounts withinteractive shells by excluding the inert ones by name:
Two of those shell names are power-off commands. The guard scans
the whole command string and cannot tell a regex alternative from
an invocation, so it denies — correctly, by its own charter.
What changed
interactive shell ends in
sh; every inert one does not. So$7 ~ /sh$/selects exactly the accounts that can log in whilenaming no inert shell at all. A fallback listing every system
account is documented for shells that break that rule (
ksh93).SKILL.mdnow states the habit rather than the one command,and fuses it with the pre-existing zsh
!~warning — bothhazards push a probe the same way.
block, so the next reader sees a real example.
CLAUDE.mdgains the general rule, becauserules/authorswrite probes too and never read the security skill.
guard-taboos-test.shpins both directions: the retiredshape must stay denied, the new one must stay allowed.
Why the probe and not the guard
Both guard-side alternatives are pre-rejected in its own header.
Argument-aware parsing "needs a per-command list of which
arguments are data, which is open-ended and would reopen issue
#4". A read-only carve-out for awk over
/etc/passwdrepeats thecategory error of issues #5, #6 and #7 — naming the spelling of a
target instead of the effect on it — and #6 established that an
interpreter's read and write "look alike from outside, so the
read-only exemption cannot be proven". The guard is a coarse
backstop by design; the probe is the layer allowed to be precise.
The new spelling is also independently better: the file's existing
note about awk's
!~not surviving zsh quoting was alreadypushing toward a positive match.
Verification
sh .claude/hooks/guard-taboos-test.sh— 221 passed, 0 failed.that exact line, and only that line.
returns
rootandpostgres(the latter ships with/bin/bashfrom
postgresql-common.postinst, which is why it is now namedas an expected exception). A counter-check over all 33 system
accounts confirms the remaining 31 are nologin/false/sync.
Not in this PR
VERSIONandCHANGELOG.mdare deliberately untouched — aversion bump drives the automatic tagging workflow, which is the
maintainer's call.
Follow-ups found in the same class (verified, not fixed here)
Three documented procedures are denied by the guard as written.
Each was confirmed against the current tree, not assumed:
rules/deployment.md:79-84— the "install the public keyon the server" block is denied four ways, and its last line,
chown -R deploy:deploy /home/deploy/.ssh, is literally theguard's own issue-Note that brew upgrade leaves the old service process running #7 example. Rephrasing cannot fix this one:
the commands genuinely are the guarded effect applied to a key
the model just created. It needs a decision (a documented
override, or handing the block to the operator), which is why
it is not in this PR.
rules/secrets.md:38-51—file …/ssh_host_ed25519_keyand
ssh-keygen -lf ….pubeach pass alone; batched into onecall the whole-string scan denies them. Confirmed by feeding
the batch to the hook directly.
SKILL.mdactively tells youto batch.
rules/cloud-image.md— declares editingsshd_configinan offline image a legitimate exception, in prose only. It
mentions
HEINZEL_GUARD_DISABLEzero times, whilerules/os-replacement.md:34andCLAUDE.md:129document theoverride. A PreToolUse deny does not read prose.
Happy to send these as separate PRs if you want them.