Skip to content

feat: hand a harness its own flags without a -- separator - #30

Merged
bojieli merged 2 commits into
mainfrom
feat/harness-flag-passthrough
Aug 30, 2026
Merged

bojieli merged 2 commits into
mainfrom
feat/harness-flag-passthrough

Conversation

@bojieli

@bojieli bojieli commented Aug 30, 2026

Copy link
Copy Markdown
Owner

What this changes

reach build-box claude --dangerously-skip-permissions failed until now:

flag provided but not defined: -dangerously-skip-permissions
Usage of claude:
  -allow-local-file-tools
    ...

Every harness flag needed -- in front of it. That is a tax on every launch, and the failure it produces when forgotten is worse than verbose — the flag package's message lands on top of reach's own usage block, which reads as reach rejecting a Claude Code flag rather than merely declining to forward one.

It was also already documented as working. The README has carried reach build-box --mode mirror --fresh claude --resume since the target-first form landed, and the comment on splitTargetArgs says --resume is handed to claude. It was not: the launcher rejected it one layer further down.

parseHarnessFlags replaces parseFlags in the seven launchers (claude, codex, kimi, goose, gemini, crush, grok). It walks the arguments itself instead of handing them to flag.Parse wholesale: a name reach defines for that launcher is consumed — with the next word too, unless the flag is boolean or was written with = — and everything else is forwarded in the order it was typed, so a forwarded flag and its value stay adjacent.

reach build-box claude --dangerously-skip-permissions
reach build-box codex --model gpt-5-codex
reach build-box --mode mirror --fresh claude --resume   # the README's example, now true

Two names stay reach's. -- still separates, because forwarding by default creates exactly one collision — a harness flag whose name reach also defines — and there has to be a way to say which was meant: reach claude -- --session x gives --session to Claude Code. And -h/--help stay reach's, so reach claude --help describes the launcher rather than starting the agent; its usage block now names reach's flags and says what happens to the rest.

The other commands keep rejecting an unknown flag. reach up, exec, fs, opencode and harness verify have no other program to hand one to, and a silently swallowed --nmae is the failure parseFlags was written to prevent.

A drift test parses the launchers' source and fails if one goes back to parseFlags, since a new adapter added that way would compile, pass its own tests, and refuse its harness's flags.

Also cuts 0.5.0: minor rather than patch because a command line that was an error now launches an agent. Nothing that worked before means anything different, and no on-disk format moves — a session written by this build loads unchanged in 0.4.0 and the other way round.

How this was verified

Unit tests cover forwarding, order preservation, string-vs-bool value consumption, --, --help, and reach's own flags still erroring on a missing value.

End to end with a fake harness on PATH against a local:// session, in both the session-name and target-first forms:

$ reach crush --session e2e --force --yolo --model sonnet -p "fix it"
CRUSH ARGV:
  [--yolo]
  [--model]
  [sonnet]
  [-p]
  [fix it]

$ reach crush --session e2e --force -- --session theirs
CRUSH ARGV:
  [--session]
  [theirs]

reach's own --session and --force were consumed; everything else arrived verbatim.

Checklist

  • make check and make lint pass (0 issues, tests under -race)
  • make integration passes, if this touches transports or file operations — n/a, this is argument parsing above the transport layer
  • Comments explain why where the code looks odd

Harness adapter: no seam is touched. This changes what reaches a harness's argv, not how reach gets inside it — no adapter, seam or launch guard is modified, so no new conformance test or docs/RESEARCH.md entry is owed.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Ni2bjhvWtMMCFN5BD6zxmP

bojieli and others added 2 commits August 30, 2026 22:31
`reach build-box claude --dangerously-skip-permissions` is the command an
operator actually wants to type, and until now it failed:

    flag provided but not defined: -dangerously-skip-permissions
    Usage of claude:
      -allow-local-file-tools
        ...

Every harness flag needed `--` in front of it. That is a tax on every launch,
and the failure it produces when forgotten is worse than verbose: it prints
reach's own usage block under the flag package's message, which reads as reach
rejecting a Claude Code flag rather than merely declining to forward one. The
README has documented `reach build-box --mode mirror --fresh claude --resume`
since the target-first form landed, and `splitTargetArgs` says in a comment
that `--resume` is handed to claude. It was not; the launcher rejected it one
layer further down.

parseHarnessFlags replaces parseFlags in the seven launchers. It walks the
arguments itself rather than handing them to flag.Parse wholesale: a name reach
defines for that launcher is consumed — with the next word too, unless the flag
is boolean or was written with `=` — and everything else is forwarded in the
order it was typed, so a flag and its value stay adjacent.

Two names stay reach's. `--` still separates, because forwarding by default
creates exactly one collision — a harness flag whose name reach also defines —
and there has to be a way to say which was meant: `reach claude -- --session x`
gives --session to Claude Code. And -h/--help stay reach's, so that
`reach claude --help` describes the launcher rather than starting the agent;
its usage block now names reach's flags and says what happens to the rest.

The other commands keep rejecting an unknown flag. `reach up`, `exec`, `fs`,
`opencode`, `harness verify` have no other program to hand one to, and a
silently swallowed `--nmae` is the failure parseFlags was written to prevent.
A drift test parses the launchers' source and fails if one goes back to
parseFlags, since a new adapter added that way would compile, pass its own
tests, and refuse its harness's flags.

Verified end to end with a fake harness on PATH against a local:// session:
`reach crush --session e2e --force --yolo --model sonnet -p "fix it"` reaches
crush as `--yolo --model sonnet -p "fix it"`, in both the target-first and
session-name forms, and `-- --session theirs` still reaches it verbatim.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ni2bjhvWtMMCFN5BD6zxmP
A minor rather than a patch because the command line gains behaviour: a flag
reach does not define is now forwarded to the harness instead of rejected, so
`reach build-box claude --dangerously-skip-permissions` launches an agent where
it used to print "flag provided but not defined".

Nothing that worked before means anything different. reach consumes exactly the
flags it always defined for each launcher, `--` still hands everything after it
to the harness, and no session document, capability document or on-disk format
moves — a session written by this build loads unchanged in 0.4.0 and the other
way round.

Nothing at a harness seam changed, which this project's entries are expected to
say one way or the other: the release is above the adapter layer and touches no
adapter, seam or launch guard. Version bumps for the helper cache path, which is
keyed on it so a release never reuses a stale remote binary.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ni2bjhvWtMMCFN5BD6zxmP
@bojieli
bojieli merged commit 4792e09 into main Aug 30, 2026
23 checks passed
@bojieli
bojieli deleted the feat/harness-flag-passthrough branch August 30, 2026 14:37
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