Launching & isolating apps
Everything enwiro launches in an environment goes through one command:
enw wrap. It is the single chokepoint where an environment’s working
directory, its ENWIRO_ENV variable, and (optionally) container isolation are
applied before your program starts.
enw wrap <COMMAND> [ENVIRONMENT] [-- [COMMAND_ARGS]...]enw wrap bash my-project runs bash inside the my-project environment. If
you omit the environment name, enwiro resolves the
active environment (and sets it up on demand if it
doesn’t exist yet).
How a launch is resolved
Section titled “How a launch is resolved”enw wrap does two things, in two different places:
- The CLI resolves (and, on demand, cooks) the environment. This turns the
environment name into a concrete project path. It stays in the
enwprocess because cooking is interactive and local. - The daemon decides how to launch. The CLI hands the resolved
(name, path, command, args)to the daemon over its RPC socket (launch.resolve). The daemon is the single source of truth for the launch decision: it answers with the final program, arguments, and environment variables. The CLI thenexec-replaces itself with that result, so your terminal (the tty) stays attached to the launched process.
flowchart TD
A["enw wrap COMMAND [ENV]"] --> B["CLI resolves / cooks the environment"]
B --> C{"daemon reachable?"}
C -- "no" --> H["Command runs unwrapped<br/>(not in the environment)"]
C -- "yes" --> D["daemon decides how to launch"]
D --> E{"container image for this env?"}
E -- "no" --> F["Command runs on host, in the environment"]
E -- "yes" --> G["Command runs in container isolation"]
click G "#the-container-isolation-path-experimental" "How container isolation runs your command"
The container branch runs your command inside a per-environment image; see the container isolation path for the exact invocation.
When the daemon answers (host or container), the program runs with its working
directory set to the environment’s path and ENWIRO_ENV set to the environment
name, so tools and shells can detect which environment they are in. The
daemon-down fallback is the exception: it runs bare.
The host path (default)
Section titled “The host path (default)”Out of the box, the daemon returns the command unchanged: it runs on the host,
in the environment’s directory, with ENWIRO_ENV set. This is the behaviour you
get without any isolation build flag.
The container isolation path (experimental)
Section titled “The container isolation path (experimental)”enwiro can instead run your command inside a container, one image per environment. This is off by default and gated two ways:
- The daemon must be built with the
container-wrapfeature (see below). - A local OCI image named
enwiro/<environment-name>must exist. Its presence is the trigger; building it is out of band (you bring your own image). If no such image exists, the launch falls back to the host path.
When both hold, the daemon returns a container invocation roughly equivalent to:
<engine> run --rm -it \ --mount type=bind,source=<env-path>,target=<env-path> -w <env-path> \ -e ENWIRO_ENV=<env-name> \ enwiro/<env-name> <command> [args...]- Engine: Podman only. (Rootless Docker lacks
--userns=keep-id, which the non-root-user handling below relies on, and rootless Podman’s networking has no host-bindable “bridge gateway” the way Docker’s does, so supporting both would mean two silently-different code paths behind one build flag.) - The project directory is bind-mounted at the same path it has on the host (and
used as the working directory), so paths match and file watching/HMR work on a
Linux host. A
--mountis used rather than-v src:dstso a path containing a colon is not mis-parsed. -itis used when the caller’s stdin is a terminal,-iotherwise.- If the project directory is itself a symlink (enwiro’s own per-environment layout uses one, so an environment keeps a stable address across re-cooks), the real path behind it is bind-mounted too, alongside the symlink path. Some tools hard-code the real absolute path into their own metadata - a git worktree’s main repo, for instance, references the worktree’s real path in its own internal bookkeeping - and need it to resolve inside the container.
- Cookbooks can also declare that an environment depends on additional host
paths beyond its own project directory - e.g. a git worktree’s main repo,
which holds the shared object database the worktree’s
.gitpoints into. Each declared path is mounted at its own identical host location.
For git worktrees specifically: mounting a worktree’s main repo mounts its whole
.git, including the object database every branch’s commits live in. So this isn’t scoped to just this worktree - any committed content on any branch of the repo is already reachable from inside the container (git show/checkoutany commit), andgit worktree listjust makes the other worktrees’ names and commit hashes easy to find (others showprunable, since their checkout paths aren’t mounted, but their commits are). Only uncommitted changes sitting in another worktree’s own working directory stay inaccessible.
The image tag is
enwiro/<name>, with the environment name sanitized into a valid OCI repository component first (lowercased, invalid characters collapsed to-) — so a GitHub-issue env named e.g.myrepo#513looks forenwiro/myrepo-513. This is a best-effort, lossy mapping, not a collision-free encoding: e.g.my#envandmy.envsanitize to the same tag.
Running with the isolation build flag
Section titled “Running with the isolation build flag”The container path lives behind the container-wrap Cargo feature on the
enwiro-daemon crate, so it is only available from a source build. Follow the
development setup first; its just install-dev recipe
already builds the feature in (it passes
--features enwiro-daemon/container-wrap) and restarts the daemon. Because the
path is still image-gated, nothing changes until you create a trigger image.
To build just the daemon by hand instead:
cargo build --release -p enwiro-daemon --features container-wrap.
Running under a different OCI runtime (microVM isolation)
Section titled “Running under a different OCI runtime (microVM isolation)”By default the container path uses whatever OCI runtime the engine defaults
to (typically runc or crun). To run containers inside a lightweight VM
instead – a stronger isolation boundary, since each container gets its own
guest kernel rather than sharing the host’s – set container_runtime in
~/.config/enwiro/enwiro.toml:
container_runtime = "krun"krun is crun’s own
libkrun-backed OCI runtime handler:
a drop-in --runtime swap, no separate CLI or exec model, verified working
rootless with genuine KVM-backed isolation (a launched container’s kernel
version differs from the host’s). A bare name like krun is resolved the same
way the container engine itself is (a PATH lookup at launch time), so this
value stays portable across machines with different install layouts – prefer
it over a hardcoded path like /usr/bin/krun unless you specifically need to
pin one of several installed copies. Any --runtime-compatible value works,
not just krun – crun/runc pins that runtime instead of the engine’s
default, for example.
Linux only: krun has no macOS build (it’s a Linux-native OCI runtime tied to
KVM), so this setting has no equivalent there yet.
Restart the daemon after changing it (systemctl --user restart enwiro-daemon.service), since it’s read once at startup. This is a single
global toggle applied to every containerized launch, not a per-environment or
per-app setting.
krunignores--userns=keep-id/--userentirely – a container always runs asuid=0in its own guest kernel regardless of those flags, so enwiro skips them specifically forkrun. It solves the problem those flags exist for (root-owned files leaking onto the host) a different way:krundoes its own uid-mapping for the bind-mounted share independent of--userns. Any other configured runtime still gets the normal--userns=keep-id/--usertreatment.
Try it end to end
Section titled “Try it end to end”# 1. Build + install with the feature (restarts the daemon)just install-dev
# 2. Create a trigger image for a simple-named environment, e.g. "my-project"echo 'FROM debian:stable-slim' | podman build -t enwiro/my-project -
# 3. Launch into it: you land in the container, at the bind-mounted project direnw wrap bash my-project
# An environment with no matching image still runs on the host:enw wrap bash some-other-envTo turn the container path off again for an environment, remove its image
(podman rmi enwiro/my-project).
Running Claude Code in isolation
Section titled “Running Claude Code in isolation”Running an agent like Claude Code inside the container was a motivating use case for this layer: the agent sees only the environment’s project directory, not the rest of your machine. Two enwiro-specific pieces make it work smoothly.
Authentication, without the credential entering the container. A naive
approach would inject your token as an environment variable, but anything running
in the container (including the agent, if it is led astray by a prompt injection)
could then read and exfiltrate it. Instead, the daemon runs a small host-side
auth proxy: a claude launch is pointed at it via ANTHROPIC_BASE_URL and given
a random capability token minted fresh for that launch, and the proxy swaps in
your real token on the host before forwarding to Anthropic, rejecting any request
that doesn’t present a capability it minted itself. The real credential never
enters the container. Configure it once:
# Mint a long-lived token tied to your subscription, then store it host-side:claude setup-tokenmkdir -p ~/.config/enwiroprintf '%s\n' 'PASTE_THE_TOKEN' > ~/.config/enwiro/claude_oauth_tokenchmod 600 ~/.config/enwiro/claude_oauth_tokenWith a token configured and an enwiro/<env> image that ships claude,
enw wrap claude <env> authenticates through the proxy using your subscription.
The routing is installed as a claude shim on the container’s PATH, so running
claude from a shell inside the environment (for example after enw wrap kitty <env>) works the same way. Only claude is affected; other commands are left
alone.
First-run onboarding is skipped automatically (see the note below), so the session lands straight at the prompt.
This is experimental and intended for your own, trusted environments only. Known limits: a capability, once minted, stays valid for the daemon’s lifetime (no per-container revocation or expiry yet), it protects the credential but not Claude’s server-side tools such as web search (those run on Anthropic’s infrastructure and never traverse the proxy), and running an agent against untrusted code in a shared kernel is not a strong security boundary. Treat it accordingly.
Notes and limits
Section titled “Notes and limits”- The daemon must be running. It is the source of truth for how a command is
launched. If it is down,
enw wrapdoes not half-wrap: it prints an error to stderr, shows a desktop notification, and execs the command bare, with no environment directory, noENWIRO_ENV, and no isolation. - Terminal emulators are wrapped specially. A recognised terminal (currently kitty only) runs on the host with the environment’s shell wrapped inside it, so the terminal needs no display passthrough. This is an experimental pilot.
- The OCI runtime is configurable, one setting for every environment. See
Running under a different OCI
runtime above –
by default it’s the engine’s own default runtime; setting
container_runtimeswaps in something likekrunfor microVM-level isolation instead. - Claude Code is authenticated through a host-side proxy so the credential never enters the container. See Running Claude Code in isolation above.
- First-run onboarding is skipped automatically. Claude Code has no env var
or setting to skip its first-run wizard (theme picker and “trust this folder”
prompt); the only lever is a
.claude.jsonmarkinghasCompletedOnboardingand the workspace’shasTrustDialogAccepted. Rather than make you bake that into every image, the container launch seeds a default.claude.jsonat start if one is absent (keyed to the environment’s directory; it never overwrites one the image already ships), soclaudein a fresh container goes straight to the prompt. enw wrapis the only launch path that consults the daemon today. Other ways enwiro starts programs (enw runvia an adapter,enw :<gear>cli entries, and the daemon’s cook-autorun) still launch on the host and do not yet go throughlaunch.resolve.