Skip to content

rawkode/cuenv

Repository files navigation

cuenv

CI codecov License: MIT Crates.io

A direnv alternative that uses CUE packages for environment configuration.

Development

Development is only supported through the Nix flake:

# Enter the development shell
nix develop

# Build the project
cargo build

Installation

Using Nix (Recommended)

# Install directly from the GitHub repository
nix profile install github:rawkode/cuenv

# Or run without installing
nix run github:rawkode/cuenv -- --help

# Using a specific version/commit
nix profile install github:rawkode/cuenv/<commit-sha>

Home Manager Module

If you're using Home Manager, you can use the included module:

# In your flake.nix
{
  inputs = {
    cuenv.url = "github:rawkode/cuenv";
    # ... other inputs
  };

  outputs = { self, nixpkgs, home-manager, cuenv, ... }: {
    homeConfigurations.yourUsername = home-manager.lib.homeManagerConfiguration {
      # ... your configuration
      modules = [
        cuenv.homeManagerModules.default
        {
          programs.cuenv = {
            enable = true;
            # Optional: specify package
            # package = cuenv.packages.${pkgs.system}.default;

            # Shell integrations (auto-detected based on enabled shells)
            # enableBashIntegration = true;
            # enableZshIntegration = true;
            # enableFishIntegration = true;
            # enableNushellIntegration = true;  # Experimental
          };
        }
      ];
    };
  };
}

The module will automatically:

  • Install the cuenv package
  • Set up shell integration for enabled shells (bash, zsh, fish, nushell)
  • Configure the shell hooks to load CUE environments automatically

Using Cargo

# Install from crates.io
cargo install cuenv

Building from Source

# Clone the repository
git clone https://github.com/rawkode/cuenv
cd cuenv

# Build with Nix
nix build

# The binary will be available in ./result/bin/cuenv

Setup

Add the following to your shell configuration:

Bash (~/.bashrc)

eval "$(cuenv init bash)"

Zsh (~/.zshrc)

eval "$(cuenv init zsh)"

Fish (~/.config/fish/config.fish)

cuenv init fish | source

Usage

  1. Create a CUE package in your project directory with .cue files:
package env

import "github.com/rawkode/cuenv"

env: cuenv.#Env & {
    DATABASE_URL: "postgres://localhost/mydb"
    API_KEY: "secret123"
    DEBUG: "true"
    PORT: "3000"
}
  1. Navigate to the directory and the environment will be automatically loaded.

Commands

  • cuenv - Load CUE package from current directory
  • cuenv load [directory] - Manually load environment from a directory
  • cuenv unload - Unload the current environment
  • cuenv status - Show environment changes
  • cuenv hook <shell> - Generate shell-specific hook output
  • cuenv init <shell> - Generate shell initialization script
  • cuenv run <command> [args...] - Run a command in a hermetic environment with only CUE-defined variables

Features

  • Automatic environment loading when entering directories
  • CUE package-based environment loading
  • Shell variable expansion support
  • Support for multiple shells (bash, zsh, fish)
  • Type-safe configuration with CUE
  • Secret resolution from 1Password and GCP Secrets Manager (with cuenv run)
  • Automatic secret obfuscation in stdout/stderr to prevent accidental exposure
  • Environment-specific configurations with inheritance
  • Capability-based variable filtering for secure credential management
  • Command inference for automatic capability detection
  • Environment variable configuration (CUENV_ENV, CUENV_CAPABILITIES)

CUE File Format

Your CUE package should use the cuenv package schema:

package env

import "github.com/rawkode/cuenv"

env: cuenv.#Env & {
    // String values
    DATABASE_URL: "postgres://user:pass@host/db"

    // String representations of numbers
    PORT: "3000"
    TIMEOUT: "30"

    // String representations of booleans
    DEBUG: "true"
    ENABLE_CACHE: "false"

    // Shell expansion is supported
    LOG_PATH: "$HOME/logs/myapp"

    // CUE features are supported
    BASE_URL: "https://api.example.com"
    API_ENDPOINT: "\(BASE_URL)/v1"  // String interpolation
    HOST: "localhost"
    DATABASE_DSN: "postgres://\(HOST):5432/myapp"  // Computed values
}

How It Works

  1. When you cd into a directory, cuenv checks for CUE packages (directories with .cue files)
  2. It loads the CUE package from the current directory
  3. Environment variables are set in your shell
  4. When you leave the directory, the environment is restored

Running Commands in Hermetic Environment

The run command executes programs with only the environment variables defined in your CUE files (plus PATH and HOME for basic functionality):

# Run a command with CUE-defined environment
cuenv run node server.js

# Pass arguments to the command
cuenv run npm -- install --save-dev

# Run shell commands
cuenv run bash -- -c "echo PORT=\$PORT"

# The environment is hermetic - parent environment variables are not passed through
export PARENT_VAR=123
cuenv run bash -- -c 'echo "PARENT_VAR=$PARENT_VAR"'  # Will print: PARENT_VAR=

Access Restrictions

You can configure disk and network access restrictions for tasks using the security section in your CUE task definitions. This uses Landlock (Linux Security Module) for enforcement:

tasks: {
  "secure-build": {
    description: "Build the project with restricted filesystem access"
    command:     "echo 'Building project securely...' && sleep 1 && echo 'Build complete!'"
    security: {
      restrictDisk: true
      readOnlyPaths: ["/usr", "/lib", "/bin"]
      readWritePaths: ["/tmp", "./build"]
    }
  }
  "network-task": {
    description: "Task that needs network access but with restrictions"
    command:     "echo 'Downloading dependencies...' && curl --version"
    security: {
      restrictNetwork: true
      allowedHosts: ["api.example.com", "registry.npmjs.org"]
    }
  }
  "fully-restricted": {
    description: "Task with both disk and network restrictions"
    command:     "echo 'Running in secure sandbox'"
    security: {
      restrictDisk: true
      restrictNetwork: true
      readOnlyPaths: ["/usr/bin", "/bin"]
      readWritePaths: ["/tmp"]
      allowedHosts: ["localhost"]
    }
  }
  "unrestricted": {
    description: "Task without security restrictions"
    command:     "echo 'Running without restrictions' && ls -la /"
  }
}

Running tasks with security restrictions:

# Run a task with disk restrictions
cuenv run secure-build

# Run a task with network restrictions
cuenv run network-task

# Run a fully restricted task
cuenv run fully-restricted

Landlock Requirements:

  • Linux kernel 5.13+ (for filesystem restrictions)
  • Linux kernel 5.19+ (for network restrictions - basic support)
  • Appropriate permissions to use Landlock LSM

Security Configuration Options:

  • restrictDisk: Enable filesystem access restrictions
  • restrictNetwork: Enable network access restrictions
  • readOnlyPaths: Array of paths allowed for reading
  • readWritePaths: Array of paths allowed for reading and writing
  • denyPaths: Array of paths explicitly denied (overrides allow lists)
  • allowedHosts: Array of network hosts/CIDRs allowed for connections

Security Model: When disk restrictions are enabled, you must explicitly allow all paths your task needs access to. This includes:

  • Executable paths (/bin, /usr/bin)
  • Library paths (/lib, /usr/lib, /lib64, /usr/lib64)
  • Configuration paths (/etc if needed)
  • Working directories and output paths

Note: Network restrictions are currently limited by Landlock V2 capabilities. The implementation will be enhanced in future versions to provide more granular network access control.

Note: Network and process restrictions are not yet fully implemented with Landlock. Use system-level controls or container runtimes for those restrictions.

Secret Resolution

When using cuenv run, secret references in your CUE files are automatically resolved:

package env

import "github.com/rawkode/cuenv"

env: cuenv.#Env & {
    // Regular environment variables
    DATABASE_HOST: "localhost"
    DATABASE_USER: "myapp"

    // Secret references - 1Password format
    DATABASE_PASSWORD: cuenv.#OnePasswordRef & {ref: "op://Personal/database/password"}
    API_KEY: cuenv.#OnePasswordRef & {ref: "op://Work/myapp-api-key/field"}

    // Secret references - Various providers
    GITHUB_TOKEN: "github://myorg/myrepo/GITHUB_TOKEN"
    AWS_SECRET: "aws-secret://prod/api/secret"
    GCP_SECRET: "gcp-secret://myproject/db-password"
    AZURE_KEY: "azure-keyvault://myvault/keys/mykey"
    VAULT_TOKEN: "vault://secret/data/myapp/token"

    // You can compose URLs with resolved secrets
    DB_HOST: "prod.example.com"
    DATABASE_URL: "postgres://\(DATABASE_USER):\(DATABASE_PASSWORD)@\(DB_HOST):5432/myapp"
}

Requirements:

  • For 1Password: Install 1Password CLI and authenticate with op signin
  • For GCP Secrets: Install gcloud CLI and authenticate with gcloud auth login

Note: Secret resolution only happens with cuenv run. Regular cuenv load will not resolve secrets for security reasons.

Secret Obfuscation

When using cuenv run, any resolved secret values are automatically obfuscated in the command's stdout and stderr output. This prevents accidental exposure of sensitive information in logs or terminal output.

# Example: If DATABASE_PASSWORD resolves to "secret123"
cuenv run sh -c 'echo "Password is: $DATABASE_PASSWORD"'
# Output: Password is: ***********

# Secrets are obfuscated even in error messages
cuenv run sh -c 'echo "Error: Failed to connect with $API_KEY" >&2'
# Stderr: Error: Failed to connect with ***********

This obfuscation applies to all resolved secrets from 1Password and GCP Secrets Manager, helping maintain security when running commands with sensitive data.

Environments and Capabilities

cuenv supports environment-specific configurations and capability-based filtering:

package env

import "github.com/rawkode/cuenv"

env: cuenv.#Env & {
    // Base configuration
    DATABASE_URL: "postgresql://localhost:5432/myapp"
    LOG_LEVEL: "info"
    PORT: "3000"

    // AWS capabilities - tagged with @capability
    AWS_REGION: "us-east-1" @capability("aws")
    AWS_ACCESS_KEY: "aws-access-key" @capability("aws")
    AWS_SECRET_KEY: "aws-secret-key" @capability("aws")

    // Docker capabilities
    DOCKER_REGISTRY: "docker.io" @capability("docker")
    DOCKER_IMAGE: "myapp:latest" @capability("docker")

    // Environment-specific overrides
    environment: {
        production: {
            DATABASE_URL: "postgresql://prod-db:5432/myapp"
            LOG_LEVEL: "warn"
            PORT: "8080"
            AWS_REGION: "us-west-2" @capability("aws")
        }
        staging: {
            DATABASE_URL: "postgresql://staging-db:5432/myapp"
            LOG_LEVEL: "debug"
        }
    }

    // Capability mappings for automatic inference
    capabilities: {
        aws: {
            commands: ["terraform", "aws", "deploy"]
        }
        cloudflare: {
            commands: ["terraform"]
        }
        docker: {
            commands: ["deploy"]
        }
    }
}

Usage:

# Use production environment
cuenv run -e production -- ./app

# Enable specific capabilities
cuenv run -c aws -- aws s3 ls

# Use environment variables
CUENV_ENV=production CUENV_CAPABILITIES=aws cuenv run -- terraform apply

# Automatic capability inference from command
cuenv run -e production -- aws s3 ls  # Automatically enables 'aws' capability

Documentation

Differences from direnv

  • Uses CUE instead of shell scripts for configuration
  • Type-safe configuration files
  • No need for direnv allow (can be added if needed)
  • Simpler mental model - just key-value pairs in CUE format

About

No description, website, or topics provided.

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Contributors 3

  •  
  •  
  •