Skip to content

The dev container's documented switches do nothing, and a cold rebuild is indistinguishable from a hang #59

Description

@amiralis1365

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

"postCreateCommand": {
  "fix-crlf": "find … -exec sed -i 's/\\r$//' {} +",
  "setup": ["bash", ".devcontainer/scripts/post-create.sh"]
}

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions