A Nix shell hook for automatically decrypting SOPS-encrypted files and exporting their contents as environment variables.
- Secure: Built-in protection against command injection and path traversal
- Fast: Efficient decryption and parsing
- Flexible: Supports multiple file formats (dotenv, JSON)
- Protected: System variables (PATH, HOME) are never overwritten by default
- Tested: Comprehensive test suite with real SOPS encryption
This project provides a Nix function that creates shell hooks for decrypting SOPS-encrypted files in development environments. The implementation includes built-in security hardening with configurable options.
# flake.nix
{
  inputs = {
    nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
    sops-decrypt-hook.url = "github:brittonr/sops-decrypt-hook";
  };
  outputs = { self, nixpkgs, sops-decrypt-hook, ... }:
    let
      system = "x86_64-linux";  # or "aarch64-darwin" for M1 Macs
      pkgs = nixpkgs.legacyPackages.${system};
    in {
      devShells.${system}.default = pkgs.mkShell {
        buildInputs = [ pkgs.sops pkgs.jq ];  # jq needed for JSON format
        shellHook = (sops-decrypt-hook.lib.mkSopsDecryptHook {
          sopsFiles = [ ./secrets.enc.env ];
        }).shellHook;
      };
    };
}The mkSopsDecryptHook function accepts the following options:
- sopsFiles: List of SOPS-encrypted files to decrypt
- protectedVars: List of environment variables that cannot be overwritten (default: system variables)
- validatePaths: Validate file paths for security (default:- true)
- validateKeys: Validate variable names (default:- true)
- maxFileSize: Maximum file size in bytes (default: 10MB)
- failOnError: Exit if decryption fails (default:- false)
- verbose: Show detailed output (default:- false)
- allowOverwrite: Allow overwriting existing environment variables (default:- false)
- globalPrefix: Add prefix to all exported variables (default:- "")
shellHook = (sops-decrypt-hook.lib.mkSopsDecryptHook {
  sopsFiles = [ ./secrets.enc.env ./database.enc.env ];
  failOnError = true;
  verbose = true;
  globalPrefix = "APP_";
}).shellHook;The hook supports multiple file formats:
# Create secrets.env
API_KEY=secret123
DATABASE_URL=postgresql://localhost/mydb
# Comments are supported
EMPTY_VALUE=
QUOTED="value with spaces"
# Encrypt with SOPS
sops -e secrets.env > secrets.enc.env{
  "api_key": "secret123",
  "database": {
    "host": "localhost",
    "port": 5432,
    "credentials": {
      "user": "admin",
      "password": "secret"
    }
  }
}# Encrypt with SOPS
sops -e secrets.json > secrets.enc.jsonNested JSON objects are automatically flattened with underscore separators:
- api_key→- api_key
- database.host→- database_host
- database.credentials.user→- database_credentials_user
Note: Use the keyTransform option to convert to uppercase if needed.
shellHook = (sops-decrypt-hook.lib.mkSopsDecryptHook {
  sopsFiles = [];  # Use fileConfigs instead for format specification
  fileConfigs = [
    { path = ./secrets.enc.env; format = "dotenv"; }
    { path = ./config.enc.json; format = "json"; prefix = "APP_"; }
  ];
}).shellHook;Note: JSON format requires jq to be available in your shell environment.
The project includes comprehensive tests implemented as Nix derivations.
# Run all checks
nix flake check
# Run specific test category
nix build .#checks.x86_64-linux.security
nix build .#checks.x86_64-linux.quick
nix build .#checks.x86_64-linux.unitTests
# Run nix-unit tests
nix run .#test-nix-unit
# Run all tests
nix run .#test-all- Quick Tests: Fast smoke tests for basic functionality
- Security Tests: Validates security protections
- Unit Tests: Mock SOPS tests for functionality
- Integration Tests: Real SOPS encryption tests with age keys
- Edge Cases: Special characters, injection attempts
- Performance: Performance benchmarks
# Default development environment
nix develop
# With sops-nix integration
nix develop .#withSopsNix
# Testing environment
nix develop .#testing{
  imports = [ sops-decrypt-hook.nixosModules.default ];
  
  services.sopsDecryptHook = {
    enable = true;
    files = [ /etc/secrets/app.env ];
    services = [ "myapp" "database" ];
  };
}{
  imports = [ sops-decrypt-hook.homeManagerModules.default ];
  
  programs.sopsDecryptHook = {
    enable = true;
    files = [ ~/.secrets/personal.env ];
    shells = [ "bash" "zsh" ];
  };
}Symptom: DATABASE_URL=postgresql://user:pass@localhost/db becomes just postgresql://user
Solution: The current implementation properly handles cut -d '=' -f 2- to preserve values with equals signs.
Symptom: Shell becomes unusable after loading secrets
Solution: System variables are protected by default. Use protectedVars option to customize the list.
Symptom: Secrets aren't loaded but no error is shown
Solution: Use verbose: true and failOnError: true options for debugging.
- Validate SOPS files before adding to your devShell
- Use appropriate failure modes:
- failOnError: truefor CI/CD pipelines
- failOnError: falsefor development (default)
 
- Use prefixes to namespace variables: globalPrefix = "MYAPP_"
- Regular testing - Run the test suite after updates
- Keep secrets files small - Default max size is 10MB
The implementation includes built-in protection against:
- Command injection via malicious variable names or values
- Path traversal attacks
- Overwriting critical system variables
- Loading of suspiciously large files
- Invalid variable names that could cause shell issues
Protected variables by default include:
- System paths: PATH,LD_LIBRARY_PATH
- Shell variables: HOME,USER,SHELL,IFS
- Security-sensitive: LD_PRELOAD,BASH_ENV,PYTHONPATH