Fix what review found in the skill: five real defects

Adversarial review of #29 found bugs I confirmed by reproducing each one.

**Every hand-written error message was unreachable.** `tok=$(curl ...)` is a
plain assignment, so `set -e` acts on the command substitution before the
following `|| die` can run. A wrong password produced exit 22 and no output at
all — the most likely way this gets used wrongly, and the least explained.
All four captures now go through a `run` helper that takes a *description*
rather than echoing the command, because one of them carries the account
password in `-u`.

**`up` was not idempotent, and the second run destroyed DNS.** The resolv.conf
backup was copied unconditionally, so `up --full` twice overwrote the good
backup with PIA's own resolvers; the later `down` then "restored" those and
left the container with no working DNS and no way back. `up` now runs `down`
first. Verified: two `up --full` runs, then `down`, and the backup still holds
the original 192.168.65.7.

**An empty gateway produced total connectivity loss, reported as healthy.**
`$gw` was never validated and `add_route` swallowed every failure to /dev/null.
The two half-routes need no gateway and would succeed, so the tunnel captured
everything while the exclusions keeping DNS and the Docker host reachable
silently did not exist — and `status` still printed "full tunnel". Routes are
now fatal on failure, and a via-less default (`$3` is the literal "eth0") is
rejected.

**The PIA session token was in the process arguments** — confirmed in `ps` and
/proc/*/cmdline, a ~24h bearer credential for the account readable by anything
in the container. It now goes to curl on stdin as a config. Verified: 60 polls
across a full `up`, zero sightings.

**The preflight diagnosed the wrong kernel module.** It checked /dev/net/tun
and blamed the tun module, but kernel WireGuard is a netlink interface and does
not use it — verified by creating one with NET_ADMIN and no tun device. The
check is dropped (the container could not have started without the device
anyway) and `ip link add` now reports the real dependency.

Also: a full tunnel with no DNS servers from PIA used to warn and carry on,
which is a tunnel leaking every lookup while reporting itself healthy — now
fatal. `down` validates the backup before restoring it, so a truncated one
cannot leave the container with no resolver at all. `wg.priv` is shredded on
teardown and created under umask 077, because /run rides `docker commit` into
the snapshot image. A mistyped `up --ful` is rejected instead of silently
giving a test route.

entrypoint: `install_feature_skill` gets `local`, a blank-name guard (the
disabled branch would otherwise `rm -rf` the whole skills directory under a
persisted volume), `-e`/`-L` so a leftover *file* at the destination is cleaned
up, and a chown of the parent so `claude` can still add skills of their own
when Mission Control is off. When the base image predates the skill it now says
so instead of returning silently — and `/opt/triple-c-skills` joins
FEATURE_PROBES so the migration pre-flight reports it. Docs corrected to match:
neither half reaches an existing project without a migration.

`vpn_env_var` extracted and tested, pinning the property the whole removal path
rests on — that the variable is emitted as 0 rather than omitted.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-17 14:04:39 -07:00
co-authored by Claude Opus 5
parent cb848e110e
commit 49ac673045
8 changed files with 244 additions and 74 deletions
+25 -6
View File
@@ -347,18 +347,37 @@ fi
# Copied on every start rather than only when absent, so a fix to a skill
# reaches projects that already have the old copy. Local edits under these
# directories do not survive — treat /opt/triple-c-skills as the source.
#
# The source lives in the *base image*, so a project whose container predates it
# recreates from its own snapshot and has no /opt/triple-c-skills to copy from.
# That case says so rather than returning silently: the toggle is on, the
# capability is there, and the skill simply never appears — which is impossible
# to work out from the outside.
install_feature_skill() {
_name=$1
_enabled=$2
_dest="/home/claude/.claude/skills/$_name"
local _name="$1"
local _enabled="$2"
local _src="/opt/triple-c-skills/$1"
local _dest="/home/claude/.claude/skills/$1"
# A blank name would make the disabled branch `rm -rf` the whole skills
# directory, Mission Control's included, under a persisted volume.
[ -n "$_name" ] || { echo "entrypoint: install_feature_skill called with no name"; return 1; }
if [ "$_enabled" = "1" ]; then
[ -d "/opt/triple-c-skills/$_name" ] || return 0
if [ ! -d "$_src" ]; then
echo "entrypoint: $_name skill unavailable — this container's base image predates it; migrate the project to get it"
return 0
fi
mkdir -p /home/claude/.claude/skills
# Not just $_dest: when Mission Control is off nothing else creates the
# parent, so root would own it and `claude` could not add a skill there.
chown claude:claude /home/claude/.claude/skills
rm -rf "$_dest"
cp -r "/opt/triple-c-skills/$_name" "$_dest"
cp -r "$_src" "$_dest"
chown -R claude:claude "$_dest"
echo "entrypoint: $_name skill installed to ~/.claude/skills/"
elif [ -d "$_dest" ]; then
elif [ -e "$_dest" ] || [ -L "$_dest" ]; then
# -e/-L rather than -d: a leftover *file* at that path must go too.
rm -rf "$_dest"
echo "entrypoint: $_name skill removed (feature disabled)"
fi