Shell mode
mode: shell runs your app services as host shell processes instead of
Docker containers. Use it for projects that already run locally with native
commands (bun run dev, cargo run, python manage.py runserver, …) and do
not need Docker images, dependency containers, or volumes.
Shell mode keeps the full WorktreeOS worktree lifecycle — first-run setup, clone
volumes, caches, service selection, targets, runtime arguments, host-port
allocation, healthchecks, tunnels, status, logs, and stop/restart actions — but
each service is started with Bun.spawn in its own process group rather than
through docker compose. There is no Docker daemon dependency.
Shell mode shares the app.services shape with generated
mode, so the two pages overlap; this page
focuses on what differs because services run as host processes.
A complete example
Section titled “A complete example”mode: shell
clone_volumes: - .env.local
host_ports: range: start: 20000 end: 29999
app: init_script: - bun install services: api: cwd: packages/api ports: - 3000 script: - bun run dev env_file: .env environment: NODE_ENV: development DATABASE_URL: postgres://localhost:5432/api web: cwd: packages/web ports: - 5173 script: - bun run dev dependencies: - api environment: API_URL: http://localhost:${app.services.api.hostPort[3000]}
targets: frontend: - web
arguments: - API_TOKENSupported fields
Section titled “Supported fields”Per-service fields under app.services.<name>:
script— required; one or more startup commands. Commands are joined with&&and run viash -lcin a detached process group from the worktree root (or the servicecwd).cwd— working directory forscriptand the serviceinit_script. A relative path resolves against the worktree root; an absolute path is used as-is. Defaults to the worktree root.ports— logical service ports WorktreeOS allocates host ports for. A number or{ port, healthcheck?, allow_failure? }, exactly as in generated mode. See the port binding contract below.env_file— path to a.envfile (relative resolves against the worktree). Loaded into the process environment before inlineenvironment.environment— inline environment variables for the process. They overrideenv_fileand support WorktreeOS template substitution.init_script— first-time commands specific to one service, run on the host after the globalapp.init_scriptand only when the service ends up in the final startup set.dependencies— names of other services this one depends on (used for selective startup).
Supported related top-level sections:
app.init_script— first-run commands, run once per worktree as host shell commands from the worktree root.clone_volumes— files copied from the source worktree on first run.cache— global cache of first-run artifacts.targets— named service sets for selective startup.arguments— runtime arguments passed with--arg.host_ports.range— the pool host ports are allocated from.
Rejected Docker-only fields
Section titled “Rejected Docker-only fields”Because nothing runs in a container, fields that only make sense for Docker are rejected with a clear validation error in shell mode:
app.imageandapp.services.<name>.image— shell mode runs host processes, not images.deps— dependency containers are not available; run datastores yourself or declare them as additionalapp.services.app.services.<name>.volumes— there is no container filesystem to mount into.connect_npm_cache,connect_yarn_cache,connect_bun_cache— package manager cache mounts require a Docker build/run.
The compose section is also rejected; it belongs to
compose mode.
Port binding contract
Section titled “Port binding contract”A configured shell-service port is a logical port for which WorktreeOS
allocates a stable host port from host_ports.range. Nothing rewrites the
process’s listening port, so the service process must bind the allocated host
port itself. To make that possible, WorktreeOS injects two convenience
variables into each service environment, describing its first configured
port:
WOS_SERVICE_PORT— the allocated host port for the first configured service port.WOS_SERVICE_HOSTNAME— the resolved hostname for that port: the service tunnel hostname when tunnels are active,localhostotherwise.
These automatic WOS_* variables are written last, so they always win over
user-supplied values, and the WOS_* namespace is reserved. The same pair is a
shared cross-mode contract: Docker-backed
generated mode injects identical
WOS_SERVICE_PORT / WOS_SERVICE_HOSTNAME values into app service containers,
so the same service code works in both modes. The binding detail differs only
in that a shell process must bind the host port itself, whereas in Docker mode
the process binds the container port and WorktreeOS publishes the host port.
A typical single-port service reads WOS_SERVICE_PORT when it starts:
app: services: api: ports: - 3000 script: - bun run dev --port "$WOS_SERVICE_PORT"For a service with multiple ports, WOS_SERVICE_PORT describes only the
first one. Reference the others exactly with templates in environment:
${app.services.<name>.hostPort[<port>]}— the allocated host port for a specific configured port.${app.services.<name>.hostname[<port>]}— the active tunnel hostname for a specific configured port, orlocalhostwhen no tunnel is open.${app.services.<name>.url[<port>]}— the full reachable URL (scheme, host and port) for a specific configured port: the public tunnel URL when a tunnel is open, orhttp://localhost:<hostPort>otherwise.
app: services: api: ports: - 3000 - 9090 script: - bun run serve --http "$WOS_SERVICE_PORT" --metrics "$METRICS_PORT" environment: METRICS_PORT: ${app.services.api.hostPort[9090]}Templates and runtime arguments (${NAME} / ${NAME:-default}) resolve before
the process starts; references to unknown services, unconfigured ports, or
undeclared arguments fail loudly.
Related
Section titled “Related”- Generated mode — the Docker-backed
counterpart sharing the
app.servicesshape. - Services and ports
- Healthchecks
- Deploy configuration reference