Skip to content

devenv 2.2: attach to running processes and persistent out-of-tree environments

devenv 2.2 ships:

  • Attach to running processes: share one native process manager across terminals, with live status, ports, logs, and a choice to detach or stop.
  • Persistent out-of-tree environments: bind a directory to a local or remote --from source, including profiles and the source's complete devenv.yaml.
  • More reliable shell activation: automatically load hooks in fish and nushell, select the correct login shell in stripped environments, and behave predictably in multiplexers and nested shells.
  • SecretSpec 0.17 and Cachix integration: upgrade from SecretSpec 0.8, pull and push to private caches without exporting CACHIX_AUTH_TOKEN, and run the bundled SecretSpec CLI with the same profile and provider as devenv.
  • A calmer and sturdier TUI: use near-zero CPU while idle, distinguish every process state at a glance, and handle unusual terminal input safely.
  • Evaluation cache fixes: invalidate correctly when local inputs, copied sources, and files modified during evaluation change.
  • Better automation interfaces: inspect tasks as JSON, access inputs in the REPL, distinguish trace callers, and avoid streaming interactive output into coding agents.
  • Smaller installs and better diagnostics: remove unnecessary LLVM and debug-toolchain dependencies while surfacing clearer errors in terminals and CI.

One process manager, many terminals

Running devenv up while the native process manager is already active now attaches to it instead of failing (devenv#2936). The second invocation connects over the control socket, starts any requested processes that aren't running yet, and streams status, ports, and logs.

$ devenv up -d                      # start processes in the background
$ devenv up                         # attach: live status, ports, and logs
$ devenv processes attach           # watch without requesting any starts
$ devenv processes start postgres   # start one process, cold-starting a manager if needed

The manager resolves before and after ordering through its own task scheduler. An attached terminal first receives the current state and recent log output, then follows live changes. Other terminals can attach and detach independently without taking ownership away from the daemon.

Ctrl-C on an attached devenv up asks whether you want to detach and leave the processes running, or stop the whole manager. devenv processes attach always detaches on Ctrl-C.

devenv processes start <name> no longer requires a manager to be running. It can cold-start one in the background with that process and its dependency closure, equivalent to devenv up -d <name> (devenv#2930). Named starts always start the requested process, even when its default start.enable is false.

Attaching is deliberately interactive. CI jobs, piped commands, and detected coding agents report that processes are already running instead of entering a live view that could wait indefinitely.

Under the hood, the process manager became the single owner of process state, the daemon pushes attach sessions as an event stream instead of being polled, and every process launch goes through the same task graph. This fixed a family of lifecycle bugs along the way: double launches, processes vanishing from list, stuck relaunches, concurrent daemon startup, and a foreground devenv up corrupting a running daemon's PID file.

Closes devenv#971, open since February 2024.

Configure an environment once, use it anywhere

--from lets you use a devenv without checking devenv.nix into the project. In 2.1 you had to repeat the flag for every command. In 2.2, devenv allow can bind the current directory to that source:

$ cd my-project
$ devenv --from github:myorg/devenv-configs?dir=rust-web allow
$ devenv shell

Every subsequent devenv command in that directory loads the bound configuration. The native shell hook auto activates it on cd, just like a project with a local devenv.nix.

Profiles persist with the binding:

$ devenv --from github:myorg/devenv-configs \
    --profile backend \
    --profile observability \
    allow

An explicit --profile still takes priority when you need a one-off override.

Local sources now bring their entire configuration with them. --from path:../shared-devenv loads the source's devenv.yaml, inputs, imports, and sibling modules from its Git repository. Modules are read from the live directory, so edits take effect without fetching or rebinding the source.

Normal in-tree projects are easier to navigate too: devenv now walks up parent directories to find devenv.nix. Commands work from any subdirectory while the shell and devenv shell -- <command> keep the directory where you invoked them (devenv#2232).

Shell activation gets out of the way

The native shell integration added in 2.1 now loads automatically in fish and nushell through their vendor configuration mechanisms. Bash and zsh have no equivalent, so they still need the one-line devenv hook setup in your shell configuration.

Shell selection is more reliable in editor terminals and stripped environments where $SHELL is missing. devenv looks up the user's login shell before falling back to bash, and warns when a shell explicitly requested through --shell or devenv.yaml is unsupported (devenv#2880, devenv#2992).

Activation and deactivation have also been hardened across real-world shell setups:

  • New tmux and zellij panes, SSH sessions, and nested shells no longer inherit a marker that can close them when they change directory (devenv#2861).
  • Leaving and immediately re-entering a project activates it again on the first try.
  • Fish preserves cd - history and works when zoxide overrides cd (devenv#2853).
  • A manually entered devenv shell, or an environment already loaded by direnv, no longer gets another devenv shell stacked on top.
  • Nushell now deactivates reliably when leaving a project.

If you still use direnv, devenv init --include-envrc (or DEVENV_INCLUDE_ENVRC) adds an .envrc to a new project. Native activation remains the default and requires no project-local activation file.

SecretSpec 0.17 and Cachix integration

devenv 2.2 upgrades its SecretSpec integration from 0.8 to SecretSpec 0.17. The release adds scopes, secrets caching, cross-secret validation, GitHub and Forgejo Actions support, and new providers including SOPS, age, KeePass KDBX, OpenBao, Scaleway Secret Manager, and systemd credentials.

Pulling from and pushing to private Cachix caches no longer requires exporting CACHIX_AUTH_TOKEN. devenv can resolve the token through SecretSpec without exposing it to the development shell:

devenv.yaml
secretspec:
  enable: true
  provider: keyring
  cachix_auth_token: true

No declaration in secretspec.toml is required. If SecretSpec does not return a token, devenv falls back to the token stored by the Cachix CLI through cachix authtoken. The resolved token is used for both pulls and the Cachix push daemon.

If your secrets backend grants access to the token under a different name, configure it in devenv.yaml:

devenv.yaml
secretspec:
  enable: true
  cachix_auth_token: CI_CACHIX_TOKEN

The string is the secret's name, not the token itself.

The matching secretspec command is now bundled with devenv. The profile and provider selected in devenv.yaml are exported as SECRETSPEC_PROFILE and SECRETSPEC_PROVIDER, so runtime commands use the same configuration that devenv used during evaluation:

$ devenv shell
$ secretspec run -- npm start

TUI: calmer, sturdier

Process status at a glance. Each process now shows a status dot whose shape encodes its lifecycle instead of an identical spinner on every row. Waiting, starting, running, ready, stopped, exited, failed, and gave-up states remain distinct. The shape carries the state, so it reads without relying on color.

Near zero idle CPU. devenv up was burning roughly 10 to 15% CPU per managed process even when everything was quiet, because the UI recomputed its layout dozens of times per second. The TUI now only redraws when something actually changes (devenv#2915).

Fuzzed nightly. The TUI gained property-based tests and a nightly terminal fuzzing run, which already flushed out crashes on multi-byte characters, progress overflow, and narrow terminals.

Better terminal rendering. Long structured logs and stack traces wrap instead of being truncated. Shell output is read in larger batches, reducing rendering overhead, and long lines copied from devenv shell no longer acquire a hard newline at the terminal's wrap point (devenv#2865).

Evaluation cache fixes

Local path: inputs now participate in cache invalidation. Editing a shared configuration applies on the next command instead of returning stale results until .devenv is deleted.

The cache also tracks local files and directories copied into the Nix store during evaluation. Directories are hashed recursively, so changes to nested scripts and imported source trees invalidate the attributes that depend on them (devenv#2886, devenv#2893).

If a tracked file changes while an evaluation is still running, that result is no longer stored. This closes a race where editing devenv.nix at the wrong moment could leave stale tasks or options behind (devenv#2745).

Other fixes cover newly created files with sub-second timestamps, SQLite caches on VM-mounted filesystems, and shells built through relocated or chrooted Nix stores (devenv#2499, devenv#2947).

For automation and coding agents

devenv down. A shorthand for devenv processes down, mirroring devenv up (devenv#2862).

devenv tasks list --json. Machine readable task graph inspection (devenv#2966).

inputs in the REPL. devenv repl now exposes inputs alongside devenv and pkgs, so you can poke at inputs declared in devenv.yaml directly (e.g. inputs.nixpkgs.lib.version).

Better agent detection. Quiet mode now kicks in for more coding agents (Aider, autonomous and cloud agents, and others) via the detect-coding-agent crate, not just Claude Code. Set DEVENV_NO_AI_AGENT=1 to opt out.

Trace callers. OpenTelemetry spans now identify whether devenv was invoked by the CLI, direnv, or the native shell hook through the devenv.caller attribute (devenv#2965). DEVENV_TRACE_DEFAULT_TO configures a default trace destination without overriding an explicit --trace-to or DEVENV_TRACE_TO.

Smaller installs and better diagnostics

Statically linking nixd, used by devenv lsp, removes the monolithic LLVM shared library and reduces the devenv closure and container image by roughly 550 MiB. Building libghostty-vt in release mode also avoids pulling a debug toolchain containing Zig and LLVM into every installation.

Non-TUI output now shows useful Nix evaluation and build progress while hiding internal debug noise. Evaluation warnings no longer replace the real error message, devenv test --no-tui preserves test output in CI, and unfree-package errors point to the relevant devenv configuration instead of generic NixOS advice.

devenv shell also terminates its inner shell when the terminal closes instead of leaving a background process at 100% CPU (devenv#2845). GitHub inputs resolve over SSH when a url.insteadOf rule rewrites them, using your SSH agent (devenv#2842).

See the full changelog for the rest.

Breaking changes

  • Dropped x86_64-darwin (Intel macOS). devenv is no longer built, tested, or released for Intel Macs. Pin an older devenv release if you're on one. x86_64-darwin environments still run through Rosetta 2 on Apple Silicon, though nixpkgs 26.05 will be its final supported nixpkgs release.
  • Auto activation detects devenv.nix. The shell hook and devenv allow now look for devenv.nix instead of devenv.yaml. Projects with only a devenv.yaml no longer auto activate; add a devenv.nix to restore activation.

Final words

Open an issue or join the Discord with feedback.

Domen