security: scrub the public IPs + client hostnames this PUBLIC repo was carrying, and gate both classes - #350
Merged
Merged
Conversation
…e the class `scripts/opencode/agent/k8s.md:38` documented a real production cluster's public IP in a table, from #276. CLAUDE.md already forbids that class ("this repo is PUBLIC"), but nothing enforced it — so a repo-wide sweep found more: * the Hetzner gateway / nebula lighthouse IP, in 6 files * the operator's HOME residential IP, in 3 files * two retired Mullvad server endpoints and a third-party parking IP Scrubbed by kind, never by blanket deletion: * prose/agent-context -> the handle the repo already uses ($KC_PROD, the mx-in.zacx.dev hostname, or a named <placeholder>) * one-shot apply scripts -> required env vars, so they fail loudly rather than silently mis-editing /etc/nixos/configuration.nix * scripts/airvpn-updown (a LIVE killswitch) -> /etc/airvpn-updown.env, an optional root-owned site file. Absent, it still arms; only the lighthouse's direct bypass route + nft accept are omitted, and the arm line then reads `lighthouse=UNSET`. Documented in the `bar` skill's airvpn.md. The gate: scripts/tests/test_no_public_ips.py over scripts/testlib/public_ip_scan.py. It DELEGATES the IPv4 predicate to scripts/claude-hooks/guard_core.py rather than growing a second copy, with a seam test pinning that the commit hook and the file scan agree. Two harness bugs the positive controls caught, both of which would have shipped a scanner reporting a confident clean zero over nothing: * the skip-dir test ran against ABSOLUTE path parts, so every file in an agent worktree (.../.claude/worktrees/<id>/...) was skipped — the entire repo * a hex-only IPv6 boundary chopped fake globally-routable addresses out of C++-style `::` scope operators — 11 false positives Measured: 1 finding on a planted IPv4 and 1 on a planted IPv6, both naming scripts/opencode/agent/k8s.md:38; 0 under test, 12 passed. 🔴 This guards HEAD only. Git history still carries every value, and a history rewrite would not unpublish what is already cloned, forked or indexed. Treat the scrubbed values as disclosed and rotate/renumber if that matters. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…h out, and gate client hostnames
Addresses the adversarial audit of this PR. Three changes, in order of weight.
1. 🔴 B-1 — THE SURVIVING MUTANT. The gate's exemptions were keyed on the VALUE
alone and applied repo-wide, so the audit's M7 (add a routable IP to
scripts/data/airvpn-servers.json AND paste it into a skill doc) passed 12/12.
Both exemption sources are now keyed on (relpath, value):
* ALLOWLIST becomes 21 explicit (path, value) pins, each with its reason —
the ClickHouse VERSION string in the headroom proposal in particular
parses as a genuinely routable address in 25/8, and was pre-approving a
whole /8 in every file;
* the derived AirVPN-catalogue exemption applies only inside CATALOGUE_FILES,
and the count of endpoints the menu TEST may mirror is pinned at 2 distinct.
Two new controls reproduce M7 as units, so re-keying on the bare value again
goes red. Stale-pin accounting now also catches a pin whose value MOVED file.
2. The killswitch change is NOT in this PR any more. scripts/airvpn-updown is a
LIVE fail-closed killswitch on the workbench uplink; moving its lighthouse IP
to an env file is a runtime change that has to pass the `bar` skill's re-test
protocol on a physically reachable host. It is split into its own PR (stacked
on this branch) and this branch tracks the remaining literal as PENDING_SCRUB,
pinned by count and deleted by that PR in the same commit.
3. Client HOSTNAMES are now gated too — the other half of CLAUDE.md's "never
commit ... a real third-party hostname used as an example".
* scripts/testlib/client_host_scan.py + scripts/tests/test_no_client_hostnames.py
flag a SUBDOMAIN of a client registrable domain. Measured, not guessed: a
flag-every-FQDN scan returns 7,470 distinct tokens here and a URL-position
one still needs ~160 allowlist entries -- a permanently-red gate. The apex
is prose about a client and is deliberately not a finding; the topology
(grafana-new.*, auth.*, sish.*, review-<hash>.*) is.
* That gate is RED at the base ref: 75 occurrences, 11 distinct hosts,
22 files. All scrubbed here.
* The browser-bridge OOPIF fixture host and 9 others were test/doc-load-bearing
-> fictional *.example.test hosts. Node suite 1024/1024.
* The client Grafana host is FUNCTIONALLY load-bearing (a bar left-click and
its toast open it), so the VALUE moved out of tracked source rather than
the feature out of the bar: scripts/bar-url resolves it at click time from
~/.config/bar/urls.env, the same 0600 pattern as media.env/airvpn.env. It
fails LOUD (exit 3, naming key and file) rather than silently doing
nothing, and a seam test pins that BOTH surfaces route through it.
Also documents, in the scanner's docstring, the nine evasion forms the audit
enumerated that this gate does not catch. None is present in the repo, and the
leading-zero gap is shared with guard_core._public_ips (both delegate to
`ipaddress`), so the seam stays honest -- but green must not read as complete.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
ZacxDev
force-pushed
the
scrub-public-ips
branch
from
August 6, 2026 17:06
88a6541 to
3b07b92
Compare
…he toast half of the seam real
Both 🟡 from the delta re-audit. Same failure in two shapes: a bar button that
dies without saying anything.
F1 -- `scripts/bar-url` `load()` caught only `OSError`, and `UnicodeDecodeError`
is not one. MEASURED: a single `\xff` byte in `urls.env` escaped as an
unhandled traceback with exit 1, contradicting this module's own 🔴 contract
("FAILS LOUD ... exit 3 with a message naming the file and the key"). From a
bar click stderr goes NOWHERE, so a traceback is not loud, it is invisible --
one stray byte in a hand-maintained file restored exactly the
silent-dead-button failure this component exists to prevent. Now
`except (OSError, UnicodeDecodeError)`, with five parametrised cases (three at
the API, two through the CLI asserting exit 3 and no traceback).
F2 -- the seam guard never reached the toast half. It asserted only that the
literal `_bar_url_action("civitai_grafana")` APPEARS in `bar-status-poll`;
nothing exercised what that function RETURNS, and `test_bar_status.py`
(122 tests) asserts nothing about the civitai action. The audit measured both
mutants surviving: `"--openn " + name + "_typo"` -> 150 passed / 0 failed, and
repointing at `scripts/bar-urls` -> 135 passed / 0 failed. Both make the toast
completely inert, and the toast is the half with no other coverage.
Now four behavioural cases on the returned string: it must split to
`[<.../bar-url>, "--open", "civitai_grafana"]`; the target must exist and be
executable; running it must print the URL, exit 0 and hand that URL to
xdg-open; and an unset key must exit 3 naming the key and the file. Both
audit-measured mutants now die.
Two things found while writing that:
* the first version of the end-to-end case ran the REAL `xdg-open` and
launched a browser tab on the developer's desktop. `xdg-open` is stubbed
now -- a suite must not touch the user's session.
* the toast resolves `bar-url` out of the GIT CHECKOUT (`$DEVRC_DIR`) while
the bar block's left-click uses the NIX-DEPLOYED copy under
`~/.config/i3status-rust/scripts`. Two copies of one script, and only one
is updated by `home-manager switch`. Pinned by an invariant guard so the
asymmetry is visible in the suite; NOT changed here, because it is a
behaviour change to a live bar component.
Also closes two smuggling forms the auditor found that were NOT in
`client_host_scan.py`'s documented gap list, which read as exhaustive:
* a subdomain written as a REGEX LITERAL WITH ESCAPED DOTS was invisible
(MEASURED: escaped 0 hits, unescaped 1). A hostname matcher in JS/Python
source is one of the likeliest places a real client host gets written down,
and this repo's own scrub found a regex literal among its occurrences.
* a subdomain whose left neighbour is `_` was invisible: `_LABEL` forbade `_`,
and the lookbehind then refused to back off to the valid suffix. Realistic
shapes: markdown italics, a DNS SRV/TXT record.
FALSE-POSITIVE re-measure, since widening can LOSE matches as easily as gain
them and a loss is invisible: both patterns pointed at six domains that ARE
heavily present here (example.test, zacx.dev, example.com, homelab.lan,
workbench.lan, github.com), 519 baseline hits -> **+2, -0**. Both additions
are genuine escaped-dot regex literals in tracked test files. Hits are
reported in one canonical form (backslashes stripped) so an allowlist cannot
need an entry per escaping style.
The gap list is rewritten to say plainly that it is NOT exhaustive -- that is
the actual lesson -- and gains the encoding, separator-substitution and
non-UTF-8-file gaps it was also missing.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
ZacxDev
added a commit
that referenced
this pull request
Aug 6, 2026
…onfig, and fix the apply/doc order 🔴 DO NOT MERGE until the `bar` skill's mandatory re-test protocol has been run on a physically reachable host. See the PR body. This is the killswitch half of the public-IP scrub, split out of #350 because `scripts/airvpn-updown` is a LIVE fail-closed killswitch on the workbench's uplink and a change to it is a runtime change, not a docs change. The lighthouse IP moves out of tracked source (this repo is PUBLIC) into a root-owned 0600 /etc/airvpn-updown.env, and the four audit findings against the first version of that move are fixed: A-2 FAIL-OPEN (🔴). A malformed value -- a partially-written env file is enough -- was interpolated into the nft ruleset, making it a SYNTAX ERROR. `up` does `nft add table` + `nft flush table` BEFORE loading, so re-arming with a bad value DELETED a working killswitch, installed nothing, and exited 0. The blanket fallback carried the same value, so it failed identically. Now: * the value is validated (`valid_ip`) and an unparseable one is UNSET, loudly; * `arm_failclosed` carries NO operator-supplied text at all -- the fallback for "the primary did not parse" must not contain what broke the parse. A-4 ROOT-SOURCED CONFIG (🔴). The file was `.`-sourced as root with no owner/mode check, so a stray `LAN_SUBNET=`/`IFACE=`/`NEBULA_USER=` line rewrote the killswitch's own constants and any other shell in it simply ran. Now the file is PARSED, not sourced: owner must be root or self, no group/other write, and only `NEBULA_LIGHTHOUSE` is read. A-3 APPLY GAP (🟡). `apply-airvpn-host.sh` installed the new helper but never created, chmod'd or checked the env file, so a fresh-host apply shipped a silently degraded killswitch. It now provisions it -- BEFORE installing the helper that reads it -- and refuses rather than proceeding, mirroring how the wg conf is handled at step 1. `claude/skills/bar/airvpn.md` had the same ordering defect (the requirement was a trailing sentence AFTER the install command) and is renumbered into dependency order. Also fixed while testing this: `log()` was defined BELOW its first callers, so every rejection reason above was `log: command not found`. The guards failed safe and said nothing -- which is most of their value gone. New: `airvpn-updown check-env`, a read-only action that prints the resolved value without touching a rule or route, and `scripts/tests/test_airvpn_updown_env.py` (42 tests) built on it. 10/10 mutants killed, each by the test that owns it. Deletes the `scripts/airvpn-updown` PENDING_SCRUB entry in the same commit, which is what the ratchet in #350 requires. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
ZacxDev
added a commit
that referenced
this pull request
Aug 6, 2026
…e two unvalidated addresses Round 3 of the audit. Rebased onto current main now that #350 has merged, so the stacked-parent hazard is gone and branch CI shows the truth. P1 (🔴-equivalent) -- STEP 11 DID NOT DO WHAT THE PROTOCOL SAID. `ssh zach@10.42.0.30` is the WORKBENCH'S OWN nebula address, and step 1 puts the operator on the LAN. Followed literally it is a SELF-SSH; from another LAN host it is a same-LAN hop. Neither exercises the nebula direct-punch path that the section's own opening line says a LAN-only test cannot reach -- the path that locked the host out in #118. The step carrying the entire "off-LAN" claim never said which host to run it FROM, so it passed unconditionally. It now names the source host, carries a still-on-the-LAN guard, defines PASS / FAIL / INVALID, and says the protocol CANNOT BE COMPLETED without an off-LAN host rather than letting the self-ssh be substituted. 🟡1 -- ARMED(none), a FOURTH state my own F4 made reachable. Before the atomic load, "both loads failed" implied the table was already flushed, so NOT-ARMED was the only outcome. After F4 the PREVIOUS table survives, so `ks_present` is true with no ruleset of ours installed -- reported as `ARMED(none)` while the line above it said "uplink is NOT filtered". The surviving table was built for a different endpoint/fwmark, so it can read as armed with a dead tunnel. Now `STALE(previous)`, with both contradictory lines fixed, four states pinned pairwise distinct, and the skill's "three states" list corrected to four. 🟡3 (M12) -- the FALLBACK path's atomicity was untested. The atomicity test drove the harness with no failure injected, so it only ever saw the primary path; open-coding the old add+flush+load in `arm_failclosed` survived all 83 tests. Both callers are pinned now, every rendered load must be self-contained, and a reachability control proves the fallback really produced a second load. 🟡4 -- `$EP` and `$GW` reached the ruleset with NO validation, while `valid_ip` -- hardened for exactly this -- was applied only to the lighthouse. A gateway-less default route yields the literal `eth0` from the awk; a peer with no endpoint makes wg print `(none)`; an IPv6 endpoint survives the sed. Each failed the PRIMARY load, dropping into the blanket fallback, which has no `meta mark` accept -- the state that kills the tunnel. nft also tries to DNS-RESOLVE these tokens, as root, inside wg-quick's PostUp. Both producers validate now. 🟡2 / 🟡5 -- two comments that were false. "A failure here changes NOTHING" is true of the helper and false of the composed path (four lines later the fallback replaces what survived). And the `\r` comment still carried the theory I retracted last round -- measured false, rtrim strips it -- corrected in the test but not in the script. 🟢 M5 -- the `10#` radix pin was UNREACHABLE: the leading-zero check fired first, so deleting `10#` changed nothing and the comment credited it anyway. Rather than ship an untestable guard with corrected prose, the two checks are REORDERED: radix-pinned range check first, leading-zero second. Identical verdict for every input, and `10#` is now load-bearing -- a mutant deleting it dies on "value too great for base". 🟢 P3 / P4 / P5 -- the precondition tested EXISTENCE not up-ness (an interface left by a half-failed `wg-quick up` printed TUNNEL-UP); the install step never said to confirm the branch, which is how the deployed helper was stale in the first place; and `ARMED(fallback)` was called merely "degraded" with no instruction to bail. Folds in the auditor's REAL X11 and X15, which my reconstruction had missed: the apply-script fixture now asserts exact bytes including the trailing newline (`.strip()` erased the byte F3 is about), and the prereq-ordering test anchors on the 🔴 block's own text rather than a substring Procedure step 1 also contains. Also hardens the FIFO fixture that leaked 30 blocked processes into the operator's session, the oldest for 77 minutes: the cleanup opens the write end non-blocking and unlinks, on the green path too. 94 tests (was 85). 12 red at base 4fa6f4f, 94 green at HEAD. 32/32 mutants killed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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.
🔴 Read this first: history is NOT rewritten, and nothing here is unpublished
This PR scrubs HEAD only. Every value below is still in git history, still in every
existing clone and fork, and possibly still in GitHub's search index and whatever crawled it.
A
filter-repo/BFG rewrite was deliberately not attempted — it breaks every clone and forkand does not unpublish what is already public.
Treat every value listed here as disclosed. If any of them matters, the remedy is
renumbering or firewalling the endpoint, not this PR. What this PR buys is that the next
one cannot land.
Round 3 — the two 🟡 from the delta re-audit
Both were the same failure in two shapes: a bar button that dies without saying anything.
🟡 F1 —
bar-urldied with a traceback instead of its own loud errorload()caught onlyOSError, andUnicodeDecodeErroris not one. MEASURED: a single\xffbyte inurls.envescaped as an unhandled traceback, exit 1 — contradicting themodule's own 🔴 contract ("FAILS LOUD… exit 3 with a message naming the file and the key").
Why this is worse than it looks: from a bar click stderr goes nowhere. A traceback is not
loud, it is invisible — so one stray byte in a hand-maintained file restored exactly the
silent-dead-button failure this component exists to prevent.
Fixed with
except (OSError, UnicodeDecodeError), plus five parametrised cases (three at theAPI, two through the CLI asserting exit 3, key and path named, and no
Traceback).One of my first fixtures was vacuous:
b"\x00\x01\x02\x03"is valid UTF-8, so thatparameter never reached the bug. Replaced with bare continuation bytes (
\x80\x81\x82), andall five parameters are now confirmed red at base.
🟡 F2 — the seam guard never reached the toast half
It asserted only that the literal
_bar_url_action("civitai_grafana")appears inbar-status-poll. Nothing exercised what that function returns, andtest_bar_status.py(122 tests) asserts nothing about the civitai action. The audit measured both mutants
surviving:
"--openn " + name + "_typo"→ 150 passed / 0 failed, and repointing atscripts/bar-urls→ 135 passed / 0 failed. Both make the toast completely inert, andthe toast is the half with no other coverage. A structural check type-checks past a wrong
argument.
Four behavioural cases now: the returned string must split to
[<…/bar-url>, "--open", "civitai_grafana"]; the target must exist and be executable;running it must print the URL, exit 0 and hand that URL to
xdg-open; and an unset key mustexit 3 naming the key and the file. Both audit-measured mutants now die.
Two things found while writing that:
xdg-openand launched a browsertab on the developer's desktop.
xdg-openis stubbed now — a test suite must not touch theuser's session. (Caught because the assertion failed on
"Opening in existing browser session."appearing in stdout.)bar-url. The toast builds itspath from
$DEVRC_DIR(the git checkout); the bar block's left-click uses${scriptsDir}(the nix-deployed copy under~/.config/i3status-rust/scripts). Only thesecond is updated by
home-manager switch; the first needs agit pullon that host. So ahost whose base clone is stale has a working button and a dead toast — the same class this
round is closing. Pinned by an invariant guard (
…resolves_bar_url_from_the_repo_root_handle,green at base, labelled as such) so the asymmetry is visible in the suite. Not changed
here — it is a behaviour change to a live bar component and belongs in its own PR.
Also: two smuggling forms the gap list did not list
client_host_scan.py's "what this does not catch" list read as exhaustive, and the auditorfound two forms missing from it. Both are now fixed, not documented away:
/^metrics-internal\.<client>$/→ 0 hits; unescaped → 1. A hostname matcher in JS/Pythonsource is one of the likeliest places a real client host gets written down — and this repo's
own scrub found a regex literal among its occurrences.
_was invisible._LABELforbade_, and thelookbehind then refused to back off to the valid suffix, so
_grpc.metrics.<client>→ 0hits despite the embedded host. Realistic shapes: markdown italics, a DNS SRV/TXT record.
🔴 False-positive re-measure, because widening a scanner can lose matches as easily as
gain them and a loss is invisible — the gate just goes quieter. Both the old and the new
pattern were pointed at six domains that are heavily present here:
example.testzacx.devexample.comhomelab.lanworkbench.langithub.comBoth additions are genuine escaped-dot regex literals in tracked test files
(
frame_eval_cdp.test.mjs:181,player_buttons.test.mjs:199). Zero false positives, zerolosses. Hits are reported in one canonical form (backslashes stripped) so an allowlist cannot
need an entry per escaping style; a dedicated case re-runs the shrinking half.
The gap list now says plainly that it is not exhaustive — that is the actual lesson — and
gained the encoding, separator-substitution (
[.],${VAR}template seams) andnon-UTF-8-file gaps it was also missing.
1. The inventory (swept before anything was changed)
Scan of every tracked file, classified with Python
ipaddress. IPv4 + IPv6.PUBLIC / routable — the actual problem
scripts/opencode/agent/k8s.md,claude/skills/mailbox/SKILL.md×2,claude/skills/mailbox/reference/build-dns-forwarding.md,nix/system/apply-nebula-443.sh×4,nix/system/apply-mullvad-enable.sh×4,scripts/airvpn-updown,scripts/claude-hooks/tests/test_guard_core.py×2claude/skills/bar/airvpn.md,claudedocs/handoff-airvpn-host-tunnel-2026-07-21.md,nix/system/apply-mullvad-enable.sh×4claudedocs/handoff-airvpn-host-tunnel-2026-07-21.mdnix/system/apply-mullvad-enable.sh×6vcap.meresolves toclaudedocs/handoff-browser-bridge-2026-07-31.md,scripts/browser-bridge/tests/fixtures/oopif-rig/README.mdgetent hosts vcap.mePUBLIC but deliberately kept — pinned per file, not repo-wide
Public resolvers, the conventional dummies, a ClickHouse version string, a Chrome major in
a User-Agent fixture, and
128.0.0.0(half of the WireGuard split-default pair). 21(path, value)entries, each with a written reason. Plus ~255 AirVPN endpoints derived fromthe committed catalogue — valid only inside the catalogue and the menu test (§4).
RFC1918 / nebula overlay — reported, deliberately NOT churned
234 occurrences, 29 distinct: the LAN
192.168.50.0/24, the nebula10.42/16overlay, CNIranges, loopback ×293, link-local, multicast, CGNAT. Unroutable from the internet and
load-bearing in ~90 files.
Documentation-reserved — already safe
TEST-NET-1/2/3 and
2001:db8::, 17 occurrences; the gate allowlists these by construction.2. The scrub — by kind, not by blanket deletion
$KC_PROD,mx-in.zacx.dev).${VAR:?…}), so they fail loudly with anamed variable instead of silently mis-editing
/etc/nixos/configuration.nix.3. Hostnames — reported in the first revision, GATED now
scripts/testlib/client_host_scan.pyflags a SUBDOMAIN of a CLIENT registrable domain.🔴 Measured, not assumed. A flag-every-dotted-FQDN scan over this repo returns 7,470
distinct tokens, almost all of them code. Tightening to URL position plus a curated real-TLD
list still leaves ~160 registrable domains — every documentation link anyone ever pasted.
That gate would be permanently red, and
RULES.mdis explicit that a permanently-red gate isworse than none. The apex is not a finding (it is prose about a client, ~100 times over);
the topology is —
grafana-new.*,auth.*,sish.*,review-<hash>.*.Red at the base ref: restoring every scrubbed file to
origin/maincontent →75 occurrences, 11 distinct client subdomains, 22 files. After the scrub: green. Six of
those hosts were not in the audit's inventory — they surfaced only because the gate went
looking.
The two the operator ruled on. Test-load-bearing → fictional
*.example.test(58occurrences across 14 files; the inventory named 8, and two were escaped forms a plain
string replace does not reach — found by running the node suite). Functionally
load-bearing → the client Grafana host moved to
~/.config/bar/urls.envbehindscripts/bar-url, which fails loud (exit 3) rather than doing nothing.🔴 The button is still unverified end-to-end.
bar-urlresolves the real value on thishost and an unset key exits 3, but nobody has clicked the actual bar button — that needs a
home-manager switchand the operator's live session.4. 🔴 The gate — and the mutant that survived the first round
The audit's M7 — add a routable IP to the catalogue and paste it into a skill doc —
passed 12/12, because both exemption sources were keyed on the value alone.
ALLOWLISTis keyed on(relpath, value); the catalogue exemption applies only insideCATALOGUE_FILES, with the menu test's mirrored endpoints pinned at 2 distinct.Harness bugs caught by controls, across three rounds — the reason none of the zeros here
are taken at face value: an absolute-path skip-dir test that skipped the whole repo; a hex-only
IPv6 boundary with 11 false positives; a case-sensitive hostname regex; a right boundary
that reported
x.<domain>.evil.test; and this round, a vacuous UTF-8 fixture and a testthat opened a browser tab.
🔴 What the gates do NOT catch — in both scanners' docstrings
IP side: leading-zero octets, decimal-integer and hex-dotted forms, a quad glued to
alphanumerics, base64, split-across-lines, fullwidth dots, IPv6
%zone, and a value in afilename. None present today; the leading-zero gap is shared with
guard_core._public_ips(both delegate toipaddress), so the seam stays honest.Hostname side: see the rewritten list — and note it now says explicitly that it is not
exhaustive, because reading it as exhaustive is precisely what let two forms through.
5. Test accounting
nix build .#checks.x86_64-linux.pytests, counted, not exit-coded.🔴 This branch is 3 commits behind
main, and #356 unbroke the gate in that window. So thenumber that matters is the merged tree, not this branch in isolation (RULES.md → "gate on
the merged tree"). Integration branch off
origin/main(93f11e1) with this branch merged in:origin/main(3d04ad5)RESULT: PASS— fully green, 0 failures, 0 new skips. Node tier on the merged tree:1024/1024.
For completeness, this branch alone still reports 28 failures — 27
/usr/bin/envsandboxfailures plus the keylog
:datecase. Those are the four causes #356 fixed on main; theyare absent from the merged tree, which is the tree that will exist after merge. Attributed by
cause, not by count.
The scratch integration branch was deleted after measuring;
airvpn-killswitch-envwas nottouched this round (still at
51c799c).6. 🔴 Two things you need to know before merging
(a) The killswitch is #355, stacked on this branch
scripts/airvpn-updownis a LIVE fail-closed killswitch; moving its lighthouse IP to an envfile is a runtime change that must pass the
barskill's re-test protocol on a physicallyreachable host. This branch therefore still carries the literal, tracked as
PENDING_SCRUBatexactly 1 hit, pinned by count and never by value. #355 deletes that entry in the same
commit that removes the literal.
(b) 🔴 MERGE-ORDER COUPLING
PENDING_SCRUBfails in both directions — red if a pending leak grows, red if it is fixedwhile the entry remains. Two landmines:
scripts/airvpn-updown(1 hit) — merge security: scrub the public IPs + client hostnames this PUBLIC repo was carrying, and gate both classes #350 → 🔴 DO-NOT-MERGE-YET: airvpn killswitch — close the fail-open, stop root-sourcing the site config, fix the apply/doc order #355 in that order and it is greenthroughout. 🔴 Do not
gh pr merge --delete-branchthis PR while 🔴 DO-NOT-MERGE-YET: airvpn killswitch — close the fail-open, stop root-sourcing the site config, fix the apply/doc order #355 is open: deletinga stacked parent's branch auto-closes the child and GitHub refuses to reopen it.
scripts/claude-hooks/tests/test_guard_core.py(2 hits) — pinned at exactly 2, and theoperator has uncommitted WIP in that file. The by-count design survives that WIP (the
occurrences move, the count stays 2). But if that file is scrubbed in a different branch,
the MERGED TREE goes red on
test_pending_scrub_counts_are_exactwhile both branches aregreen alone. Whoever scrubs it must delete the entry in the same commit — a two-token
change (both occurrences → the
1.2.3.4dummy already used in that file).Tracked follow-ups (not in scope this round)
PENDING_SCRUBexempts a path wholesale, so a value swap at constant count stays green —a documented trade.
SECRETS.mddoes not list the new~/.config/bar/urls.env.bar-urlhas no scheme allowlist and no--terminator.strings— clean). Now at least stated in the hostname scanner's gap list.🤖 Generated with Claude Code