Skip to content

Say which shell is reading start.sh and stop.sh, instead of exiting 1 with nothing printed - #567

Merged
davidmckayv merged 1 commit into
mainfrom
fix-start-shell
Sep 15, 2026
Merged

davidmckayv merged 1 commit into
mainfrom
fix-start-shell

Conversation

@mxmzb

@mxmzb mxmzb commented Sep 15, 2026

Copy link
Copy Markdown
Collaborator

sh scripts/start.sh produced exit 1 and not one character of output. No line number, no failing command, no message — the whole of what somebody had to go on was the number, which reads as "this script is broken" rather than "run it the other way". It cost an afternoon before it was traced.

What was happening

Two things, one on top of the other.

The shebang says bash and sh overrides it. On macOS sh is bash, run in POSIX mode, where a failure these scripts survive under bash is fatal instead. So the run ended at the first setting read out of .env, before any output existed to say so.

That setting could not tolerate an absent key. setting reads its key with a grep pipeline, and grep finding nothing is an exit status of 1 that pipefail makes the pipeline's. set -e then ended the script on its way to the fallback sitting on the very next line — the fallback that is the entire reason setting takes a second argument. .env.example does not list APP_PORT, so every .env copied from it is missing exactly the first key the script reads.

Neither half is visible on its own. Under bash the failed status does not escape the command substitution, so the fallback wins and everything works; the trap only fires under sh, where it fires in silence.

What this changes

Both scripts source scripts/require-bash.sh before they set any options. It names the shell it found and prints the command to retype:

This script is bash, and it is being read by bash in POSIX mode, which is what `sh` is.
Run it as: bash scripts/start.sh

It is POSIX syntax throughout, because the shell it is warning about may not be bash at all — set -o pipefail is itself a bashism and a syntax error in dash, which is sh on most Linux distributions, so the refusal has to be written before it and without local, [[ or ${!name}. SHELLOPTS carrying posix is what separates bash-as-sh from bash; a BASH_VERSION check alone cannot see that case, and that case is every Mac.

And setting takes || true, so the second argument is the value an absent key gets by construction rather than by a subtlety of where set -e applies.

stop.sh had both defects identically — sh scripts/stop.sh was the same silent exit 1 — and gets the same two changes.

Tests

The refusal has a test that was watched failing against the unfixed script: sh exits non-zero, names bash, and names the command to run, on stderr.

The absent-key test passes either way, and its docblock says so rather than dressing it up. Under bash the fallback already won, and with the refusal in place there is no longer an invocation that can watch it fail — what it pins is the contract, that the second argument is what an absent key takes and no property of the reading shell may decide otherwise.

Both run through the existing harness in start-restart-guard.test.ts, which fakes .env and the binaries, so nothing here starts a stack.

Verified

bash scripts/start.sh and bash scripts/stop.sh both run the real local stack through a full stop-and-start cycle, ending with the app answering 200 on 3010. sh and dash both refuse with their own correct sentence, naming the right script in each case. bun test scripts/ 25/25, typecheck clean across all four packages, biome format and lint clean.

Nothing in the repo, the docs or the workflows invokes either script without bash.

…othing printed

`sh scripts/start.sh` produced exit 1 and not one character of output. No line
number, no failing command, no message — the whole of what somebody had to go on
was the number, which reads as "this script is broken" rather than "run it the
other way".

Two things were behind it.

The shebang says bash and `sh` overrides it. On macOS `sh` IS bash, run in POSIX
mode, where a failure these scripts survive under bash is fatal instead. So the
run ended at the first setting read out of `.env`, before any output existed to
say so.

That setting reads its key with a `grep` pipeline, and `grep` finding nothing is
an exit status of 1 that `pipefail` makes the pipeline's. `set -e` then ended the
script on its way to the fallback sitting on the next line — the fallback that is
the entire reason `setting` takes a second argument. `.env.example` does not list
`APP_PORT`, so every `.env` copied from it is missing exactly the first key the
script reads.

Both scripts now source `scripts/require-bash.sh` before they set any options,
which names the shell it found and prints the command to retype. It is POSIX
syntax throughout, because the shell it is warning about may not be bash at all —
`set -o pipefail` is itself a bashism and a syntax error in dash, which is `sh` on
most Linux distributions. `SHELLOPTS` carrying `posix` is what separates bash-as-sh
from bash; a `BASH_VERSION` check alone cannot see that case, and that case is
every Mac.

`stop.sh` had both defects identically and is fixed with the same two changes.

The refusal has a test that was watched failing. The absent-key test passes either
way and says so in its own docblock: under bash the fallback already won, and with
the refusal in place there is no longer an invocation that can watch it fail — so
what it pins is the contract rather than the fix.

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

@davidmckayv davidmckayv left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed commit 97a4978 against the local code at 554f2b2. No actionable findings. Applied the patch to a temporary copy of the local scripts: all 4 tests passed, both scripts correctly rejected sh, bash --posix, and dash, and Bash syntax checks passed. Validation used simulated processes; the live desktop stack was not restarted.

@davidmckayv
davidmckayv merged commit 1f0efd3 into main Sep 15, 2026
15 checks passed
@davidmckayv
davidmckayv deleted the fix-start-shell branch September 15, 2026 20:49
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