Problem
The dev container is the supported environment — CONTRIBUTING.md sends every
contributor to Reopen in Container and offers nothing else. A cold build of it
currently has six defects. Two make documented switches inert, three make a
slow build indistinguishable from a hung one, and one leaves the container
half-provisioned after a single missing tool.
1. MUSHER_INSTALL_CLAUDE and MUSHER_INSTALL_CODEX are documented, and read by nothing
.env.example advertises them — "Skip the AI CLI installs on a slow
connection" — and devcontainer.json declares both in containerEnv. No
script reads either one:
$ git grep -n MUSHER_INSTALL -- .devcontainer/
.devcontainer/.env.example:29:# MUSHER_INSTALL_CLAUDE=0
.devcontainer/.env.example:30:# MUSHER_INSTALL_CODEX=0
.devcontainer/devcontainer.json:64: "MUSHER_INSTALL_CLAUDE": "1",
.devcontainer/devcontainer.json:65: "MUSHER_INSTALL_CODEX": "1"
base_install_claude installs unconditionally, and mise install resolves the
whole manifest, so the pinned npm:@openai/codex installs unconditionally too.
Setting either variable to 0 changes nothing. These two installs are most of
the several minutes a cold container spends in postCreateCommand, so the one
switch a developer on a slow connection would reach for is the one that does
not work.
MUSHER_LOG_LEVEL is the same defect in miniature: documented in the same
block, read by nothing.
2. containerEnv outranks --env-file, so the switch could not have worked anyway
Even once a script reads them, the values in containerEnv win.
containerEnv becomes docker run -e, and an explicit -e beats
--env-file for the same name regardless of the order they appear in. Pinned
to "1" there, they override whatever .devcontainer/.env says — and .env
is the file .env.example tells developers to edit. Two places claim to decide
this, and the one the documentation points at is the one that loses.
3. The object-form postCreateCommand neither ordered its steps nor showed its output
Object-form lifecycle commands run their entries in parallel, and buffer
each entry's output until that entry exits. So this pair does neither of the
things its shape suggests: the CRLF strip races post-create.sh instead of
preceding it, and the setup entry prints nothing at all for the whole cold tool
install. Silence for minutes from a container that is provisioning normally is
indistinguishable from a deadlock, and it has already sent someone hunting for
one that was not there.
4. waitFor: postCreateCommand holds the connection for the entire install
Combined with (3), the developer gets a Connecting… window, no output, and no
usable terminal until every network install has finished.
5. sed -i runs on the host, where BSD sed has different syntax
Both the initializeCommand inline strip and strip_crlf() in
initialize.sh use sed -i 's/\r$//' FILE. On macOS — every macOS host — BSD
sed reads the argument after -i as the backup suffix, consumes the
expression, and then treats the file as the script. It exits non-zero:
- inline,
2>/dev/null swallows the error, so the strip silently never happens
- in
initialize.sh, set -e propagates it and takes the whole
initializeCommand down with it
This is the CRLF guard failing on the exact hosts it exists to protect.
6. Unbounded installs, and one missing tool skipping the rest of setup
Every network install runs with no ceiling — curl | sh, curl | bash,
mise install. A stalled connection parks postCreateCommand for as long as
the kernel holds the socket, which with (3) means a container that will never
finish and never say so.
Separately, base_verify_tools runs last under set -e, so one missing CLI
ends post-create.sh right there — skipping install_lefthook_hooks and
install_spec_tools, i.e. the git hooks and bun install. A container missing
one tool ends up missing its hooks and its dependencies as well. It also
verifies claude and codex unconditionally, so switching an install off
(once that works at all) would report a failure the developer asked for.
Suggested fix
- Read the switches. Default Claude Code on and Codex opt-in — Codex is
an npm package carrying a platform binary and it dominates the cold install.
Drop Codex from the resolved set with MISE_DISABLE_TOOLS rather than
editing mise.toml, so the pin stays recorded and CI still sees it.
- Remove both from
containerEnv and let the scripts supply the defaults, so
.devcontainer/.env is the single place that decides. Say why in a comment,
or someone will helpfully add them back.
- Collapse
postCreateCommand to one string joined with && — that restores
the ordering and streams output line by line — and set
waitFor: onCreateCommand so the connection is not gated on provisioning.
State the trade-off in a comment: a terminal opened in the first minute or
two will not have task, lefthook or claude on PATH yet.
perl -i -pe for the host-side strips; it means the same thing on both host
families.
- A
bounded() helper around timeout --foreground, plus
--connect-timeout/--max-time on the curls, so a stall fails instead of
hanging.
- Have verification report through a return code instead of aborting, carry
that status to the exit code, and verify only the tools this container was
actually asked to install.
- Implement
MUSHER_LOG_LEVEL — a debug() that is quiet unless it is set.
Note
Nothing here touches the specification, the schemas, or the conformance corpus.
It is entirely .devcontainer/, and task check is unaffected by it — which is
also why none of it was caught: no check covers the lifecycle configuration,
and the failures live on hosts and cold paths that CI never takes.
Problem
The dev container is the supported environment — CONTRIBUTING.md sends every
contributor to Reopen in Container and offers nothing else. A cold build of it
currently has six defects. Two make documented switches inert, three make a
slow build indistinguishable from a hung one, and one leaves the container
half-provisioned after a single missing tool.
1.
MUSHER_INSTALL_CLAUDEandMUSHER_INSTALL_CODEXare documented, and read by nothing.env.exampleadvertises them — "Skip the AI CLI installs on a slowconnection" — and
devcontainer.jsondeclares both incontainerEnv. Noscript reads either one:
base_install_claudeinstalls unconditionally, andmise installresolves thewhole manifest, so the pinned
npm:@openai/codexinstalls unconditionally too.Setting either variable to
0changes nothing. These two installs are most ofthe several minutes a cold container spends in
postCreateCommand, so the oneswitch a developer on a slow connection would reach for is the one that does
not work.
MUSHER_LOG_LEVELis the same defect in miniature: documented in the sameblock, read by nothing.
2.
containerEnvoutranks--env-file, so the switch could not have worked anywayEven once a script reads them, the values in
containerEnvwin.containerEnvbecomesdocker run -e, and an explicit-ebeats--env-filefor the same name regardless of the order they appear in. Pinnedto
"1"there, they override whatever.devcontainer/.envsays — and.envis the file
.env.exampletells developers to edit. Two places claim to decidethis, and the one the documentation points at is the one that loses.
3. The object-form
postCreateCommandneither ordered its steps nor showed its outputObject-form lifecycle commands run their entries in parallel, and buffer
each entry's output until that entry exits. So this pair does neither of the
things its shape suggests: the CRLF strip races
post-create.shinstead ofpreceding it, and the setup entry prints nothing at all for the whole cold tool
install. Silence for minutes from a container that is provisioning normally is
indistinguishable from a deadlock, and it has already sent someone hunting for
one that was not there.
4.
waitFor: postCreateCommandholds the connection for the entire installCombined with (3), the developer gets a Connecting… window, no output, and no
usable terminal until every network install has finished.
5.
sed -iruns on the host, where BSD sed has different syntaxBoth the
initializeCommandinline strip andstrip_crlf()ininitialize.shusesed -i 's/\r$//' FILE. On macOS — every macOS host — BSDsed reads the argument after
-ias the backup suffix, consumes theexpression, and then treats the file as the script. It exits non-zero:
2>/dev/nullswallows the error, so the strip silently never happensinitialize.sh,set -epropagates it and takes the wholeinitializeCommanddown with itThis is the CRLF guard failing on the exact hosts it exists to protect.
6. Unbounded installs, and one missing tool skipping the rest of setup
Every network install runs with no ceiling —
curl | sh,curl | bash,mise install. A stalled connection parkspostCreateCommandfor as long asthe kernel holds the socket, which with (3) means a container that will never
finish and never say so.
Separately,
base_verify_toolsruns last underset -e, so one missing CLIends
post-create.shright there — skippinginstall_lefthook_hooksandinstall_spec_tools, i.e. the git hooks andbun install. A container missingone tool ends up missing its hooks and its dependencies as well. It also
verifies
claudeandcodexunconditionally, so switching an install off(once that works at all) would report a failure the developer asked for.
Suggested fix
an npm package carrying a platform binary and it dominates the cold install.
Drop Codex from the resolved set with
MISE_DISABLE_TOOLSrather thanediting
mise.toml, so the pin stays recorded and CI still sees it.containerEnvand let the scripts supply the defaults, so.devcontainer/.envis the single place that decides. Say why in a comment,or someone will helpfully add them back.
postCreateCommandto one string joined with&&— that restoresthe ordering and streams output line by line — and set
waitFor: onCreateCommandso the connection is not gated on provisioning.State the trade-off in a comment: a terminal opened in the first minute or
two will not have
task,lefthookorclaudeonPATHyet.perl -i -pefor the host-side strips; it means the same thing on both hostfamilies.
bounded()helper aroundtimeout --foreground, plus--connect-timeout/--max-timeon the curls, so a stall fails instead ofhanging.
that status to the exit code, and verify only the tools this container was
actually asked to install.
MUSHER_LOG_LEVEL— adebug()that is quiet unless it is set.Note
Nothing here touches the specification, the schemas, or the conformance corpus.
It is entirely
.devcontainer/, andtask checkis unaffected by it — which isalso why none of it was caught: no check covers the lifecycle configuration,
and the failures live on hosts and cold paths that CI never takes.