fix(claude.sh): anchor the workspace argument by position #26

Merged
claudiomaradonna merged 2 commits from fix/argument-parsing into main 2026-07-29 08:44:01 +02:00

The problem

The wrapper multiplexes two argv namespaces — its own flags and everything
forwarded to the claude CLI — without knowing the arity of Claude's flags.
Phase 1 resolved that with a filesystem test: any bare argument naming an
existing directory became the workspace.

The value of a Claude option is a bare argument, so:

  • claude . -p docs mounted ./docs instead of ., and -p reached Claude
    with its value consumed.
  • claude . -p /etc bind-mounted /etc into the container.
  • A second directory silently replaced the first.

Nothing warned, because claude . -p docs and claude . -p "fix it" are
structurally identical — only the state of the filesystem distinguishes them.
That defeats the single guarantee the wrapper makes about what the container
can see.

The fix

The workspace is now position-anchored. A bare word can claim it only while the
wrapper is still parsing its own arguments; anything bound for Claude locks the
slot. Since a flag always precedes its value, an option value can no longer be
read as a mount.

Input Action Locks?
-- Consume; forward everything after it verbatim yes
Known wrapper flag Consume, with its value no
Unknown flag (-*) Forward to Claude yes
Bare word, unlocked, is a directory Becomes the workspace; a second is an error no
Bare word, unlocked, not a directory Forward to Claude yes
Bare word, locked Forward to Claude

Two bare directories now produce an error naming both ways to disambiguate,
rather than a silent choice between them. The resolved workspace is printed to
stderr next to the session line — a wrong mount was previously invisible until
something misbehaved inside the container.

before after
claude . -p docs mounted ./docs, -p lost its value mounts ., forwards -p docs
claude . -p /etc bind-mounted /etc mounts ., forwards -p /etc
claude . docs silently mounted docs exit 1, podman not invoked

What does not change

Every invocation in README.md and --help parses identically — each places
the directory first or after a known wrapper flag, which does not lock. All
nine were verified end-to-end against the real script, not just in tests.

Breaking change

claude "fix it" . no longer mounts .; it forwards both arguments and falls
back to cwd, because the prompt locks the slot. This ordering is undocumented,
but it is a real break for anyone who types prompts that way. It follows
necessarily from position-anchoring and was accepted deliberately during design.

Testing

Adds tests/test_arg_parsing.sh — 7 tests for the new behaviour, 10 pinning the
documented invocations. 43 tests green on this branch.

All 7 were watched failing before the fix. One had its assertions edited
afterwards, which destroys that proof, so the suite was additionally verified
against two mutations of the parser (removing the -- case; dropping the lock
on unknown flags) to confirm it catches both. Flake-checked with 30 full-suite
runs and 15 shuffled-order runs.

Review notes

Design rationale, including the two rejected alternatives (-- required
everywhere, and an explicit --workspace flag), is in
docs/superpowers/specs/2026-07-28-argument-parsing-design.md, added by the
first commit on this branch.

This branch forked from main and does not include perf/claudeignore-pruning.
The two touch different phases of claude.sh and should merge cleanly, but no
run has exercised them together — worth a suite run on main after the second
merge.

## The problem The wrapper multiplexes two argv namespaces — its own flags and everything forwarded to the `claude` CLI — without knowing the arity of Claude's flags. Phase 1 resolved that with a filesystem test: any bare argument naming an existing directory became the workspace. The value of a Claude option *is* a bare argument, so: - `claude . -p docs` mounted `./docs` instead of `.`, and `-p` reached Claude with its value consumed. - `claude . -p /etc` bind-mounted `/etc` into the container. - A second directory silently replaced the first. Nothing warned, because `claude . -p docs` and `claude . -p "fix it"` are structurally identical — only the state of the filesystem distinguishes them. That defeats the single guarantee the wrapper makes about what the container can see. ## The fix The workspace is now position-anchored. A bare word can claim it only while the wrapper is still parsing its own arguments; anything bound for Claude locks the slot. Since a flag always precedes its value, an option value can no longer be read as a mount. | Input | Action | Locks? | |---|---|---| | `--` | Consume; forward everything after it verbatim | yes | | Known wrapper flag | Consume, with its value | no | | Unknown flag (`-*`) | Forward to Claude | yes | | Bare word, unlocked, is a directory | Becomes the workspace; a second is an error | no | | Bare word, unlocked, not a directory | Forward to Claude | yes | | Bare word, locked | Forward to Claude | — | Two bare directories now produce an error naming both ways to disambiguate, rather than a silent choice between them. The resolved workspace is printed to stderr next to the session line — a wrong mount was previously invisible until something misbehaved inside the container. | | before | after | |---|---|---| | `claude . -p docs` | mounted `./docs`, `-p` lost its value | mounts `.`, forwards `-p docs` | | `claude . -p /etc` | bind-mounted `/etc` | mounts `.`, forwards `-p /etc` | | `claude . docs` | silently mounted `docs` | exit 1, podman not invoked | ## What does not change Every invocation in `README.md` and `--help` parses identically — each places the directory first or after a *known* wrapper flag, which does not lock. All nine were verified end-to-end against the real script, not just in tests. ## Breaking change `claude "fix it" .` no longer mounts `.`; it forwards both arguments and falls back to cwd, because the prompt locks the slot. This ordering is undocumented, but it is a real break for anyone who types prompts that way. It follows necessarily from position-anchoring and was accepted deliberately during design. ## Testing Adds `tests/test_arg_parsing.sh` — 7 tests for the new behaviour, 10 pinning the documented invocations. 43 tests green on this branch. All 7 were watched failing before the fix. One had its assertions edited afterwards, which destroys that proof, so the suite was additionally verified against two mutations of the parser (removing the `--` case; dropping the lock on unknown flags) to confirm it catches both. Flake-checked with 30 full-suite runs and 15 shuffled-order runs. ## Review notes Design rationale, including the two rejected alternatives (`--` required everywhere, and an explicit `--workspace` flag), is in `docs/superpowers/specs/2026-07-28-argument-parsing-design.md`, added by the first commit on this branch. This branch forked from `main` and does not include `perf/claudeignore-pruning`. The two touch different phases of `claude.sh` and should merge cleanly, but no run has exercised them together — worth a suite run on `main` after the second merge.
The Phase 1 parser treats any bare argument naming an existing directory
as the workspace. Option values are bare arguments, so `claude . -p docs`
mounts ./docs instead of . and `claude . -p /etc` bind-mounts /etc into
the container. Nothing warns, because only the state of the filesystem
distinguishes an option value from a path.

Spec anchors the workspace to the position before the first passthrough
argument, adds `--` as a terminator, and turns the one remaining
ambiguous case into a hard error. Every documented invocation is
unchanged.

Claude-Session: https://claude.ai/code/session_01NNo6M8YqCf7DrUDq6jBjF9
Any bare argument naming an existing directory became the workspace, and
the value of a claude option is a bare argument. `claude . -p docs`
mounted ./docs and handed claude a -p with nothing after it;
`claude . -p /etc` bind-mounted /etc into the container. Nothing warned,
because `claude . -p docs` and `claude . -p "fix it"` differ only in what
happens to exist on disk.

A bare word may now claim the workspace only while the wrapper is still
parsing its own arguments. Anything bound for claude locks the slot, and
since a flag always precedes its value, an option value can no longer be
read as a mount. `--` ends wrapper parsing outright, and two bare
directories are an error naming both ways to disambiguate rather than a
silent choice between them.

Every invocation in README.md and --help parses unchanged: each puts the
directory first or after a wrapper flag, which does not lock. One
undocumented ordering does change -- `claude "fix it" .` now forwards
both and falls back to cwd, because the prompt locks the slot.

The resolved workspace is printed to stderr next to the session line; a
wrong mount was otherwise invisible until something misbehaved inside
the container.

Adds tests/test_arg_parsing.sh: 7 tests for the new behaviour and 10
pinning documented invocations. Both halves were verified against
mutations of the parser, since a test that has never failed proves
nothing.

Claude-Session: https://claude.ai/code/session_01NNo6M8YqCf7DrUDq6jBjF9
claudiomaradonna force-pushed fix/argument-parsing from dd9fc8295d to c0c8d876b8 2026-07-29 08:43:44 +02:00 Compare
claudiomaradonna deleted branch fix/argument-parsing 2026-07-29 08:44:05 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
claudiomaradonna/claude-jail!26
No description provided.