Skip to content

Blog

devenv 1.8: Progress TUI, SecretSpec Integration, Listing Tasks, and Smaller Containers

devenv 1.8 fixes a couple of annoying regressions since the 1.7 release, but also includes several new features:

We’ve rewritten our tracing integration to improve reporting on what devenv is doing.

More importantly, devenv is now fully asynchronous under the hood, enabling parallel execution of operations. This means faster performance in scenarios where multiple independent tasks can run simultaneously.

The new progress interface provides real-time feedback on what devenv is doing:

devenv progress bar

We’re continuing to improve visibility into Nix operations to give you even better insights into the build process.

We’ve integrated SecretSpec, a new standard for declarative secrets management that separates secret declaration from provisioning.

This allows teams to define what secrets applications need while letting each developer, CI system, and production environment provide them from their preferred secure provider.

Learn more in Announcing SecretSpec Declarative Secrets Management.

The devenv tasks list command now groups tasks by namespace, providing a cleaner and more organized view:

Terminal window
$ devenv tasks list
backend:
└── lint (has status check)
└── test
└── build (watches: src/backend/**/*.py)
deploy:
└── production
docs:
└── generate (watches: docs/**/*.md)
└── publish
frontend:
└── lint
└── test (has status check)
└── build

You can now run tasks at any level in the hierarchy. By default, tasks run in single mode (only the specified task):

Terminal window
# Run only frontend:build (default single mode)
$ devenv tasks run frontend:build
Running tasks frontend:build
Succeeded frontend:build 5ms
1 Succeeded 5.75ms
# Run frontend:build with all its dependencies (before mode)
$ devenv tasks run frontend:build --mode before
Running tasks frontend:build
Succeeded frontend:lint 4ms
Succeeded frontend:test 10ms
Succeeded frontend:build 4ms
3 Succeeded 20.36ms
# Run frontend:build and all tasks that depend on it (after mode)
$ devenv tasks run frontend:build --mode after
Running tasks frontend:build
Succeeded frontend:build 5ms
Succeeded deploy:production 5ms
2 Succeeded 11.44ms

The CLI now supports specifying single packages via the --option flag (#1988). This allows for more flexible package configuration directly from the command line:

Terminal window
$ devenv shell --option "languages.java.jdk.package:pkg" "graalvm-oracle"

The CI container ghcr.io/cachix/devenv/devenv:v1.8 has been reduced (uncompressed) from 1,278 MB in v1.7 to 414 MB in v1.8—that’s a reduction of over 860 MB (67% smaller!).

This makes devenv container much faster to pull and more efficient in CI/CD pipelines.

Join our Discord community to share your experiences and help shape devenv’s future!

Domen

Announcing SecretSpec: Declarative Secrets Management

We’ve supported .env integration for managing secrets, but it has several issues:

  • Apps are disconnected from their secrets - applications lack a clear contract about which secrets they need
  • Parsing .env is unclear - comments, multiline values, and special characters all have ambiguous behavior across different parsers
  • Password manager integration is difficult - requiring manual copy-paste or template workarounds
  • Vendor lock-in - applications use custom parsing logic, making it hard to switch providers
  • No encryption - .env files are stored as plain text, vulnerable to accidental commits or unauthorized access

While we could recommend solutions like dotenvx to encrypt .env files or sops for general secret encryption, these bring new challenges:

  • Single key management - requires distributing and managing a master key
  • Trust requirements - everyone with the key can decrypt all secrets
  • Rotation complexity - departing team members require key rotation and re-encrypting all secrets

Larger teams often adopt solutions like OpenBao (the open source fork of HashiCorp Vault), requiring significant infrastructure and operational overhead. Smaller teams face a gap between simple .env files and complex enterprise solutions.

What if instead of choosing one tool, we declared secrets uniformly and let each environment use its best provider?

The Hidden Problem: Conflating Three Concerns

Section titled “The Hidden Problem: Conflating Three Concerns”

We’ve created SecretSpec and integrated it into devenv. SecretSpec separates secret management into three distinct concerns:

  • WHAT - Which secrets does your application need? (DATABASE_URL, API_KEY)
  • HOW - Requirements (required vs optional, defaults, validation, environment)
  • WHERE - Where are these secrets stored? (environment variables, Vault, AWS Secrets Manager)

By separating these concerns, your application declares what secrets it needs in a simple TOML file. Each developer, CI system, and production environment can provide those secrets from their preferred secure storage - without changing any application code.

One Spec, Multiple Environments, Different Providers

Section titled “One Spec, Multiple Environments, Different Providers”

Imagine you commit a secretspec.toml file that declares:

# secretspec.toml - committed to your repo
[project]
name = "my-app"
revision = "1.0"
[profiles.default]
DATABASE_URL = { description = "PostgreSQL connection string", required = true }
REDIS_URL = { description = "Redis connection string", required = false }
STRIPE_API_KEY = { description = "Stripe API key", required = true }
[profiles.development]
# Inherits from default profile - only override what changes
DATABASE_URL = { default = "postgresql://localhost/myapp_dev" }
REDIS_URL = { default = "redis://localhost:6379" }
STRIPE_API_KEY = { description = "Stripe API key (test mode)" }
[profiles.production]
# Production keeps strict requirements from default profile

Now, here’s the magic:

  • You (on macOS): Store it in Keychain, retrieve with secretspec --provider keyring run -- cmd args
  • Your teammate (on Linux): Store it in GNOME Keyring, same command works
  • That one developer: Still uses a .env file locally (we don’t judge, we’ve been there)
  • CI/CD: Reads from environment variables in GitHub Actions secretspec --provider env run -- cmd args
  • Production: Secrets get provisioned using AWS Secret Manager

Same specification. Different providers. Zero code changes.

Let’s walk through migrating from .env to SecretSpec.

First, choose your default provider and profile:

Terminal window
$ secretspec config init
? Select your preferred provider backend:
> keyring: Uses system keychain (Recommended)
onepassword: OnePassword password manager
dotenv: Traditional .env files
env: Read-only environment variables
lastpass: LastPass password manager
? Select your default profile:
> development
default
none
Configuration saved to ~/.config/secretspec/config.toml

Create secretspec.toml from your existing .env:

Terminal window
$ secretspec init --from dotenv

1. Local Development with devenv (You’re on macOS)

Section titled “1. Local Development with devenv (You’re on macOS)”

Enable SecretSpec in devenv.yaml:

secretspec:
enable: true

In devenv.nix:

{ pkgs, lib, config, ... }:
{
languages.rust.enable = true;
services.minio = {
enable = true;
buckets = [ config.secretspec.secrets.BUCKET_NAME ];
};
}

Start the minio process:

Terminal window
$ devenv up
Starting minio...
.github/workflows/test.yml
- name: Run tests
env:
DATABASE_URL: {{ secrets.TEST_DATABASE_URL }}
STRIPE_API_KEY: {{ secrets.STRIPE_TEST_KEY }}
run: |
secretspec run --provider env --profile production -- npm test
fly.toml
[processes]
web = "secretspec run --provider env --profile production -- npm start"
# Set secrets using fly CLI:
# fly secrets set DATABASE_URL=postgresql://... STRIPE_API_KEY=sk_live_...
# SecretSpec will read these from environment variables

Notice what didn’t change? Your secretspec.toml. Same specification, different providers, zero code changes.

While secretspec run provides secrets as environment variables, your application remains disconnected from knowing which secrets it requires. The Rust SDK bridges this gap by providing type-safe access to your declared secrets.

The Rust SDK provides compile-time guarantees:

// Generate typed structs from secretspec.toml
secretspec_derive::declare_secrets!("secretspec.toml");
fn main() -> Result<(), Box<dyn std::error::Error>> {
// Load secrets using the builder pattern
let secretspec = SecretSpec::builder()
.with_provider("keyring") // Can use provider name or URI like "dotenv:/path/to/.env"
.with_profile(Profile::Production) // Can use string or Profile enum
.load()?;
// Access secrets (field names are lowercased)
println!("Database: {}", secretspec.secrets.database_url); // DATABASE_URL → database_url
println!("Stripe: {}", secretspec.secrets.stripe_api_key); // STRIPE_API_KEY → stripe_api_key
// Optional secrets are Option<String>
if let Some(redis) = &secretspec.secrets.redis_url {
println!("Redis: {}", redis);
}
// Access profile and provider information
println!("Using profile: {}", secretspec.profile);
println!("Using provider: {}", secretspec.provider);
// For backwards compatibility, export as environment variables
secretspec.secrets.set_as_env_vars();
Ok(())
}

Add to your Cargo.toml:

[dependencies]
secretspec = "0.2.0"
secretspec_derive = "0.2.0"

The application code never specifies where to get secrets - only what it needs through the TOML file. This keeps your application logic clean and portable.

We’d love to see more SDKs that bring this same declarative approach to Python, JavaScript, Go, and other languages.

We’re exploring features for future workflows:

Let’s make secret management as declarative as package management. Let’s stop sharing .env files over Slack. Let’s build better tools for developers.

Share your thoughts on our Discord community or open an issue on GitHub. We’d love to hear how you handle secrets in your team.

Domen

devenv 1.7: CUDA Support, Enhanced Tasks, and MCP support

devenv 1.7 brings several practical improvements:

We’ve started work on supporting multiple Nix implementations in devenv. The codebase now includes a backend abstraction layer that will allow users to choose between different Nix implementations.

This architectural change paves the way for integrating Snix (our development fork). While the Snix backend isn’t functional yet, the groundwork is in place for building out this Rust-based reimplementation to the C++ Nix implementation. See PR #1950 for implementation details.

Here’s how to enable CUDA support only on Linux systems while keeping your environment working smoothly on macOS:

  • CUDA-enabled packages are built with GPU support on Linux
  • macOS developers can still work on the same project without CUDA
  • The correct CUDA capabilities are set for your target GPUs
devenv.yaml
nixpkgs:
config:
allowUnfree: true
x86_64-linux:
cudaSupport: true
cudaCapabilities: ["7.5", "8.6", "8.9"]

Tasks now skip execution when their input files haven’t changed, using the new execIfModified option:

{
tasks = {
"frontend:build" = {
exec = "npm run build";
execIfModified = [ "src/**/*.tsx" "src/**/*.css" "package.json" ];
};
"backend:compile" = {
exec = "cargo build --release";
execIfModified = [ "src/**/*.rs" "Cargo.toml" "Cargo.lock" ];
};
};
}

This dramatically speeds up incremental builds by skipping unnecessary work.

Run all tasks within a namespace using prefix matching:

Terminal window
# Run all frontend tasks
$ devenv tasks run frontend

devenv now includes a built-in MCP server that enables AI assistants like Claude to better understand and generate devenv configurations:

Terminal window
# Start the MCP server
$ devenv mcp

AI assistants can now:

  • Search for packages and their options
  • Understand devenv’s configuration format
  • Generate valid configurations based on your requirements
  • Shell Integration: Your shell aliases and functions now work correctly
  • Clean Mode: Fixed shell corruption when using --clean
  • Error Messages: More helpful error messages when commands fail
  • State Handling: Automatically recovers from corrupted cache files
  • Direnv Integration: Fewer unnecessary environment reloads

Standardized Language Tooling Configuration

Section titled “Standardized Language Tooling Configuration”

All language modules will support the same configuration pattern (PR #1974):

{
languages.rust.dev = {
lsp.enable = false;
debugger.enable = false;
linter.enable = false;
formatter.enable = false;
};
}

Import Rust projects and their dependencies as Nix packages with the new languages.rust.import configuration (PR #1946):

{
languages.rust.enable = true;
languages.rust.import = {
mypackage = {
root = ./.;
};
};
packages = languages.rust.import.mypackage.packages;
}

That allows us to bridge the gap between developer environments and fully packaged Rust applications using Nix.

Operations that can run in parallel will (PR #1970).

Join our Discord community to share your experiences and help shape devenv’s future.

We’re particularly interested in feedback on the standardized language tooling configuration coming in 1.8 - let us know if this approach works for your use cases!

Domen

devenv 1.6: Extensible Ad-Hoc Nix Environments

devenv 1.6 has been tagged, allowing you to:

  • Create temporary environments directly from the command line without requiring a devenv.nix file.
  • Temporarily modify existing environments.

Developer environments on demand using the new --option (-O) flag:

Terminal window
$ devenv --option languages.python.enable:bool true \
--option packages:pkgs "ncdu git ripgrep" \
shell

This command creates a temporary Python environment without writing any configuration files.

Ad-hoc environments are ideal for quickly testing languages or tools without committing to a full project setup:

Terminal window
$ devenv -O languages.elixir.enable:bool true shell iex

The --option flag supports multiple data types, making it flexible for various use cases:

  • :string for text values
  • :int for integers
  • :float for decimal numbers
  • :bool for true/false values
  • :path for file paths
  • :pkgs for specifying Nix packages

One of the most powerful applications of ad-hoc environments is in CI pipelines, where you can easily implement testing matrices across different configurations:

jobs:
test:
strategy:
matrix:
python-version: ['3.9', '3.10', '3.11']
steps:
- uses: actions/checkout@v3
- uses: cachix/install-nix-action@v31
- uses: cachix/cachix-action@v16
with:
name: devenv
- name: Install devenv.sh
run: nix profile install nixpkgs#devenv
- name: Test with Python {{ '${{ matrix.python-version }}' }}
run: |
devenv --option languages.python.enable:bool true \
--option languages.python.version:string {{ '${{ matrix.python-version }}' }} \
test

This approach lets you validate your code across multiple language versions or dependency combinations without maintaining separate configuration files for each scenario.

When used with an existing devenv.nix file, --option values override the configuration settings in the file, making it easy to temporarily modify your environment.

Ad-hoc options are perfect for switching between predefined profiles in your development environment:

Terminal window
$ devenv --option profile:string backend up

This enables you to switch between frontend, backend, or other custom profiles without modifying your configuration files.

See our Profiles guide for more details on setting up and using profiles.

For complete documentation on this feature, visit our Ad-hoc Developer Environments guide.

We’re excited to see how you’ll use ad-hoc environments to streamline your development workflow. Share your feedback on GitHub or join our Discord community!

devenv 1.5: Overlays Support and Performance Improvements

In this release, we’re introducing a powerful Nix concept: overlays for modifying and extending the nixpkgs package set, along with significant performance and TLS certificate improvements.

Overlays allow you to modify or extend the default package set (pkgs) that devenv uses. This is particularly useful when you need to:

  • Apply patches to existing packages
  • Use different versions of packages than what’s provided by default
  • Add custom packages not available in nixpkgs
  • Use packages from older nixpkgs versions

Here’s an example of using overlays in your devenv.nix file to apply a patch to the hello package:

{ pkgs, ... }:
{
# Define overlays to modify the package set
overlays = [
# Override an existing package with a patch
(final: prev: {
hello = prev.hello.overrideAttrs (oldAttrs: {
patches = (oldAttrs.patches or []) ++ [ ./hello-fix.patch ];
});
})
];
# Use the modified packages
packages = [ pkgs.hello pkgs.my-tool ];
}

Using packages from a different nixpkgs version

Section titled “Using packages from a different nixpkgs version”

You can even use packages from a different nixpkgs version by adding an extra input to your devenv.yaml:

inputs:
nixpkgs:
url: github:cachix/devenv-nixpkgs/rolling
nixpkgs-unstable:
url: github:nixos/nixpkgs/nixpkgs-unstable

And then using it in your devenv.nix:

{ pkgs, inputs, ... }:
{
overlays = [
(final: prev: {
nodejs = (import inputs.nixpkgs-unstable {
system = prev.stdenv.system;
}).nodejs;
})
];
# Now you can use the unstable version of Node.js
languages.javascript.enable = true;
}

For more details and examples, check out the overlays documentation.

TLS Improvements: Native System Certificates

Section titled “TLS Improvements: Native System Certificates”

We’ve heard from ZScaler how they are using devenv and we’ve fixed their major annoyance by ensuring devenv now respects system certificates that many enterprises rely on.

macOS Development Enhancements: Custom Apple SDK Support

Section titled “macOS Development Enhancements: Custom Apple SDK Support”

For macOS developers, we’ve added the ability to customize which Apple SDK is used for development:

{ pkgs, ... }:
{
apple.sdk = pkgs.apple-sdk_15;
}

This allows you to:

  • Control exactly which version of the SDK to use
  • Ensure consistency across development environments
  • Avoid incompatibilities between different macOS versions

Sander further tweaked the performance of developer environment activation at OceanSprint when it can be cached:

  • Linux: ~500ms -> ~150ms
  • macOS: ~1300ms -> ~300ms

Join our Discord to share feedback and suggestions!

Domen