Skip to content

Warn when config.shell is not a shell mandelbot can drive - #181

Open
phil-kremidas-unitedmasters wants to merge 1 commit into
masterfrom
validate-shell-config
Open

Warn when config.shell is not a shell mandelbot can drive#181
phil-kremidas-unitedmasters wants to merge 1 commit into
masterfrom
validate-shell-config

Conversation

@phil-kremidas-unitedmasters

Copy link
Copy Markdown
Collaborator

The bug

A user set shell in ~/.mandelbot/config.json to a custom .sh wrapper. Every project tab and the home tab then opened claude in the same hardcoded directory, silently.

The claude launcher in src/tab/stream.rs builds cd '<project dir>' && exec claude ... and hands it to the shell as -l -i -c <command>. A script ignores all four arguments and just runs its own body — including its own cd. The carefully constructed command is thrown on the floor with no diagnostic anywhere.

Detection

Structural checks only, in config.rs, ordered most-decisive first:

  1. Shebang — read the first 128 bytes of the shell's file. A shebang means it's a script, whatever the interpreter, and a script always drops the arguments passed to it. This is the decisive signal and it catches the reported case, including a wrapper with no .sh extension.
  2. POSIX allowlist by basenamesh/bash/zsh/dash/ksh/… at any path, so /opt/homebrew/bin/bash and /nix/store/…/bin/bash are fine.
  3. Known non-POSIX shellsfish, csh, tcsh, nu, pwsh, … are real shells that genuinely don't honor -l -i -c this way. Distinct message: not "this is a script".
  4. Script extension, then unrecognized name — softer warnings.

Why not a probe. Running <shell> -l -i -c 'exit 7' at startup is empirical, but it costs a subprocess on every launch and a pathological wrapper (one that reads stdin, or blocks) hangs the app before the window appears. The shebang read gets the same evidence for the realistic failure at the cost of one 128-byte file read.

Why not an allowlist alone. It wrongly rejects a valid shell at an unusual path, and it can't tell ~/bin/myshell-the-binary from ~/bin/myshell-the-wrapper. The shebang can.

Surfacing

A toast, via the existing src/toast.rs machinery, seeded in App::boot — mandelbot is a GUI app and this exact bug's signature is "looks like a mandelbot bug, nothing on stderr the user ever sees". Deliberately not auto-dismissed after the usual 10s: it stays until closed. Also printed to stderr once.

The check never refuses to start. An unusual-but-working shell still boots, just loudly.

Empty-shell panics

Both crash sites are fixed:

  • stream.rs claude path indexed shell_parts[0] unchecked
  • stream.rs plain-shell path used .expect("shell config must not be empty")

Both now fall back to default_shell() with a warning, and Config::load rewrites an empty value up front so neither should fire in practice.

Tests

13 new unit tests over the pure check_shell(shell, shebang) function — allowlist at odd paths, shell-with-arguments, empty/whitespace, shebang beating a shell-like filename, an ELF first line not being a shebang, case-insensitive extensions, #!/usr/bin/env bash resolving to bash, and every warning naming ~/.mandelbot/config.json.

cargo build and cargo test pass — 91 tests, up from 78.

Verified by hand against a .sh wrapper, an empty shell, and /bin/zsh (silent).

🤖 Generated with Claude Code

@astex
astex force-pushed the validate-shell-config branch from 36f7e3a to e155b95 Compare August 3, 2026 18:32
A `shell` set to a wrapper script silently broke every claude tab: the
claude launcher passes `-l -i -c "<cd project dir> && exec claude ..."`,
and a script ignores those four arguments entirely, running its own body
instead. Tabs opened in whatever directory the script cd'd to, with no
diagnostic anywhere.

Classify `shell` at config-load time and surface the result as a toast
that stays until dismissed (plus a stderr line). The check never refuses
to start — an unusual-but-working shell should still boot, just loudly.

Detection is structural, in this order:
- a shebang means the file is a script, whatever the interpreter, and a
  script always drops the arguments mandelbot passes it
- basename in a POSIX-shell allowlist is fine, at any path
- fish/csh/tcsh/nu/... are real shells that don't honor `-l -i -c`
- a script-ish extension, or an unrecognized name, warns more softly

Also fix two crashes on an empty `shell`: the unchecked `shell_parts[0]`
index in the claude path and the `.expect` in the plain-shell path both
panicked the tab thread. Both now fall back to `default_shell()`, and
config load rewrites an empty value up front.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@astex
astex force-pushed the validate-shell-config branch from e155b95 to 66448cc Compare August 13, 2026 16:29
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.

2 participants