diff --git a/.claude/hooks/guard-taboos-test.sh b/.claude/hooks/guard-taboos-test.sh index 9c940e6..857f2a6 100755 --- a/.claude/hooks/guard-taboos-test.sh +++ b/.claude/hooks/guard-taboos-test.sh @@ -456,6 +456,15 @@ check deny 'vi /etc/heinzel.conf' check deny 'ssh root@h "rm /etc/heinzel.conf"' check deny 'ssh root@h "sed -i s/false/true/ /etc/heinzel.conf"' check deny 'python3 -c "import os; os.remove(\"/etc/heinzel.conf\")"' +# Other spellings of the same file: a case-insensitive filesystem +# (macOS), // and /./, a relative path after cd, find -delete. +check deny 'rm /ETC/HEINZEL.CONF' +check deny 'rm /etc//heinzel.conf' +check deny 'rm /etc/./heinzel.conf' +check deny 'cd /etc && rm heinzel.conf' +check deny 'cd /etc; echo allow_changes=true > heinzel.conf' +check deny 'find /etc -name heinzel.conf -delete' +check deny 'ln -sf /dev/null /etc/heinzel.conf' check pass 'cat /etc/heinzel.conf' check pass 'ssh root@h "cat /etc/heinzel.conf"' check pass 'ssh root@h "if [ -e /etc/heinzel.conf ]; then cat /etc/heinzel.conf || echo HEINZEL_CONF_UNREADABLE; else echo HEINZEL_CONF_ABSENT; fi"' diff --git a/.claude/hooks/guard-taboos.sh b/.claude/hooks/guard-taboos.sh index 086d1c7..9983469 100755 --- a/.claude/hooks/guard-taboos.sh +++ b/.claude/hooks/guard-taboos.sh @@ -362,9 +362,14 @@ EFIVARS='/sys/firmware/efi/efivars' # The server lock. The machine's owner writes it to keep heinzel # hands-off; a heinzel that could flip it to true or delete it -# would make it worthless. Unanchored on the left, so macOS's -# /private/etc matches too. Reading it stays allowed. -LOCKFILE='/etc/heinzel\.conf' +# would make it worthless. Matched by its file name alone, in any +# case: the full path misses /ETC/HEINZEL.CONF on macOS, /etc// +# and /etc/./, and a relative heinzel.conf after cd /etc. A write +# to any file of that name is denied with it. Not closable: a +# glob (/etc/heinzel.c*) or a name built at run time still gets +# through, so the written rule stays the first line of defence. +# Reading it stays allowed. +LOCKFILE='[Hh][Ee][Ii][Nn][Zz][Ee][Ll]\.[Cc][Oo][Nn][Ff]' # A general-purpose language runtime. See the interpreter section # at the bottom for why this list, and not a list of the ways @@ -970,8 +975,9 @@ account work is a Heinzel Pro feature" fi # --- The server lock ------------------------------------------ -# Same write set as sudoers above: a redirect, sed -i, an editor, -# a delete, move or re-permission, a copy or tee onto it. Moving +# Same write set as sudoers above (a redirect, sed -i, an editor, +# a delete, move or re-permission, a copy or tee onto it), plus +# find -delete, which deletes by a name it never spells as a path. Moving # or deleting it lifts the lock as surely as writing true. if hit "$LOCKFILE"; then if hit '>>?[[:space:]]*["'\'']?[^[:space:];|&]*'"$LOCKFILE" \ @@ -979,6 +985,7 @@ if hit "$LOCKFILE"; then && hit '(^|[[:space:]])-i'; } \ || hit "(^|[^[:alnum:]_-])$EDITOR([^[:alnum:]_-]|\$)" \ || hit "(^|[^[:alnum:]_-])($CLOBBER|cp)([^[:alnum:]_-]|\$)" \ + || hit '(^|[[:space:]])-delete([[:space:]]|$)' \ || writes_to "$LOCKFILE" then deny "/etc/heinzel.conf is the server lock; only the \ diff --git a/CHANGELOG.md b/CHANGELOG.md index eeb4e77..92bdb5c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## 3.1.1 — 2026-10-15 + +- **The server lock holds under other spellings.** The + guard now matches `heinzel.conf` by name in any case, + so `/ETC/HEINZEL.CONF` on macOS, `/etc//heinzel.conf` + and a relative path after `cd /etc` are blocked too, + and so is `find … -delete`. + ## 3.1.0 — 2026-10-15 - **A server can lock itself.** `allow_changes = false` diff --git a/VERSION b/VERSION index fd2a018..94ff29c 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -3.1.0 +3.1.1