Skip to main content

Execute commands with secrets loaded encrypted from your system keyring

Project description

GitHub Tag Lint Test PyPI - Version GHCR Tag

kleys 🔐

κλείς (kleís) — Greek for "key". Because your secrets should live in a keyring, not a .env file.

Execute commands with secrets fetched encrypted from your keyring, eliminating permanent .env files on disk. Works on Linux, macOS, and Windows.

kleys is a CLI tool that stores secrets encrypted (Fernet/AES-128-CBC, enabled by default) in your system's keyring, then loads them at runtime to run your commands — eliminating .env files from disk. Perfect for developers who want to keep credentials off the filesystem while maintaining a smooth development workflow.

Quick Example

Instead of this (storing secrets on disk):

# ❌ Dangerous: secrets exposed on filesystem
cat .env  # DATABASE_PASSWORD=super_secret
python app.py

Do this (secrets from keyring):

# ✅ Secure: secrets loaded from keyring, never persisted to disk
kleys python app.py

Under the hood: kleys retrieves your secrets from the system keyring and passes them to your command — no permanent .env files on disk. It has three modes: file mode (default) writes a temp .env with secure permissions and deletes it after; file descriptor mode (@SECRETS@) passes secrets via an in-memory FD with zero disk writes; export mode (--export) exports secrets as real environment variables without writing any file.

🔒 Encryption by default: All secrets are encrypted with Fernet (AES-128-CBC + HMAC-SHA256) before being stored in the keyring. An attacker who enumerates your keyring gets ciphertext, not plaintext. The decryption key is never stored in the keyring. Use --unencrypted only when necessary.

Why You Need This

Four threats this eliminates at once. Security and usability — no trade-off.

1. File-harvesting malware. Supply-chain attacks and post-exploitation tools scan disk for .env files and exfiltrate them. With --export or @SECRETS@, the file never exists on disk — nothing to steal.

2. The .env in git accident. One wrong git add . and credentials are in your repository history forever. No .env file on disk means nothing to stage, commit, or push.

3. Process-table leaks. The common pattern export $(cat .env | xargs) spawns cat, xargs, and echo subprocesses whose command-line arguments expose your secrets to any user running ps aux. --export passes secrets directly in the subprocess environment — no intermediate processes, no command-line arguments, no process-table leaks.

4. LLM data exposure. AI coding agents with filesystem access can read .env files and act on credentials — even with safety instructions in place. And secrets included in LLM prompts may be retained in training data or exposed through breaches. No file on disk means nothing for agents to find and nothing to leak through a prompt.

kleys --export ansible-playbook site.yml    # no file = no malware, no git risk
kleys --export ./deploy.sh                   # no subprocess = no ps leaks
kleys --export npm run dev                   # all four, every time

Prerequisites

  • Python 3.11+
  • A keyring service: GNOME Keyring, KWallet, macOS Keychain, or Windows Credential Manager

Installation

From PyPI (Recommended)

pip install kleys

Or with uv:

uv tool install kleys

Or with pipx:

pipx install kleys

From source

git clone https://github.com/hugobatista/kleys.git
cd kleys
pip install .

Docker

kleys also runs on Docker — see Docker Guide for standalone and host keyring workflows.

Usage

kleys [OPTIONS] COMMAND [ARGS...]
kleys show [--key KEY] [--password PASSWORD]
kleys clear [--key KEY]

Subcommands

Subcommand Aliases Description
run Execute a command with secrets from the keyring (default when no subcommand given)
show list Display all stored secrets for a key

| clear | delete, rm | Delete all stored secrets for a key |

Run Options

Option Description
--secrets-file FILE, -f FILE Path used to expose secrets to the subprocess in file mode (default mode). A temp file is created at this path with the secrets and SECRETS_FILE env var points to it. If FILE already exists, kleys offers to import it into the keyring instead (default: .env)
--key KEY, -k KEY Keyring entry identifier (default: current folder name)
--export, -e Export secrets as environment variables directly to the subprocess (overrides file mode, no temp file created). The secrets content must be in valid KEY=VALUE format per line — the user is responsible for correct formatting.
--password PASSWORD Encrypt secrets with a password (Fernet/AES-128-CBC). If omitted, resolves from KLEYS_PASSWORD env var or prompts.
--unencrypted, -u Disable encryption, store/retrieve secrets as plaintext (default: encryption enabled).

Show / Clear Options

Option Description
--key KEY, -k KEY Keyring entry identifier (default: current folder name)
--password PASSWORD Decryption password (prompts if omitted and needed)

Environment

Variable Description
KLEYS_PASSWORD Encryption password used automatically for encrypt/decrypt when set, unless overridden by --password PASSWORD.

Encrypted by default (Fernet/AES-128-CBC). Use --unencrypted to opt out (not recommended).

Modes of Operation

kleys has three modes for passing secrets to your command:

Mode How to enable How secrets arrive Writes to disk?
File (default) No flag Temp .env file, SECRETS_FILE points to it Temp file, auto-deleted
File Descriptor @SECRETS@ token in args In-memory FD as /dev/fd/9, SECRETS_FILE=/dev/fd/9 Never (Unix only)
Export --export / -e flag Exported as environment variables in the subprocess environment Never

Pick the mode that matches how your tool reads secrets. The examples below show each mode in action.

For detailed data flow and how secrets traverse each mode, see Architecture.

Examples

Example 1: Python development with uv

kleys uv run pywrangler dev

What happens:

  1. Loads .env from keyring for current folder
  2. Creates temporary .env file
  3. Runs uv run pywrangler dev with secrets available
  4. Deletes .env after command completes

Example 2: Python project with hatch

kleys hatch run dev

Perfect for running development servers where you need environment variables but don't want them persisted on disk.

Example 3: Ansible playbook with environment variables

kleys --export ansible-playbook site.yml

Before kleys:

source .env && ansible-playbook site.yml

What happens with --export:

  1. Loads secrets from keyring (or uses local .env file if it exists)
  2. Parses every KEY=VALUE pair and sets them in the subprocess environment
  3. Runs ansible-playbook site.yml with all env vars available
  4. No temp file is needed when loading from keyring — secrets stay in memory

Useful for any tool that expects secrets as environment variables — Ansible, Terraform, custom scripts, etc.

Example 4: GitHub Actions local testing with act

kleys --secrets-file .secrets act --secret-file .secrets

What happens:

  1. Uses custom file name .secrets instead of .env
  2. Loads or prompts for secrets under that filename
  3. Runs act with the secrets file
  4. Cleans up .secrets after execution

This is especially useful for testing GitHub Actions workflows locally while keeping production secrets secure.

Example 5: Multiple environments with custom key names

# Development environment
kleys --key myproject-dev npm start

# Production environment
kleys --key myproject-prod npm start

Each --key name is a separate keyring entry, allowing you to manage different secret sets (dev, staging, prod) for the same project.

Example 6: Docker commands

kleys docker-compose up

Great for docker-compose files that source .env for configuration.

Example 7: Inspecting the secrets file path

# File mode: SECRETS_FILE points to the temp file
kleys bash -c 'echo "Secrets file: $SECRETS_FILE"'

# FD mode: SECRETS_FILE points to the file descriptor
kleys bash -c 'echo "Secrets FD: $SECRETS_FILE"' --secret-file @SECRETS@

The SECRETS_FILE environment variable is set in both file and FD modes.

Example 8: File descriptor mode (no disk I/O)

Note: FD mode (@SECRETS@) is Unix/macOS only — not supported on Windows. Use --export or --secrets-file on Windows instead.

kleys act --secret-file @SECRETS@

What happens:

  1. Detects @SECRETS@ token in arguments
  2. Loads secrets from keyring into memory
  3. Creates file descriptor at /dev/fd/9 (no disk write)
  4. Replaces @SECRETS@ with /dev/fd/9
  5. Runs act which reads secrets from the file descriptor
  6. FD automatically closes — no cleanup needed

Perfect for:

  • GitHub Actions local testing with act
  • Docker with --env-file
  • Any tool that can read from file descriptors

Won't work for:

  • Shell sourcing (source $SECRETS_FILE)
  • Tools that verify file exists with stat checks
  • Tools that need to read the file multiple times

Example 9: Docker with file descriptor mode

kleys docker run --env-file @SECRETS@ myimage

Secrets are loaded from keyring and passed to Docker without ever touching the disk. The @SECRETS@ token automatically enables zero-disk-I/O mode.

Example 10: View stored secrets

kleys show
kleys show --key myproject

Displays all secrets stored for the current (or specified) app. Decrypts automatically when needed.

Example 11: Delete stored secrets

kleys clear
kleys clear --key myproject

Deletes all secrets (both encrypted and plaintext) for the app. Useful for resetting or rotating credentials.

Advanced Features

Custom Secrets File Locations

# Use a different file name
kleys --secrets-file .env.production npm run build

# Use a path in a different directory
kleys --secrets-file /tmp/my-secrets ./deploy.sh

SECRETS_FILE Environment Variable

In file mode and FD mode, your command receives SECRETS_FILE pointing to the secrets source:

# File mode: points to temp .env
kleys bash -c 'echo "Secrets are at: $SECRETS_FILE"'
# FD mode: points to /dev/fd/9
kleys bash -c 'echo "Secrets are at: $SECRETS_FILE"' --secret-file @SECRETS@

In export mode (--export), SECRETS_FILE is not set — the secrets are already in the environment.

File Descriptor Mode (No Disk I/O)

Note: FD mode is Unix/macOS only — not supported on Windows. Use --export or --secrets-file instead.

For maximum security, use the @SECRETS@ token in your command to pass secrets via file descriptor without writing to disk:

kleys act --secret-file @SECRETS@

How it works:

  • kleys detects the @SECRETS@ token in your command arguments
  • Loads secrets from keyring into memory only
  • Creates file descriptor at /dev/fd/9 (no disk write)
  • Replaces @SECRETS@ token with /dev/fd/9 in all arguments
  • Your command reads from the FD as if it were a file
  • No temp file created, no cleanup needed
  • FD automatically closes when command completes

Security benefits:

  • Zero disk I/O — secrets never touch the filesystem
  • No directory entry visible in ls
  • Automatic cleanup (pipe closes on exit)
  • No permission race conditions
  • Simple, explicit syntax — just use @SECRETS@ where you need it

Compatibility:

Works with these tools:

kleys act --secret-file @SECRETS@
kleys docker run --env-file @SECRETS@ image

Replaced tokens work just like file paths:

kleys mycommand --config @SECRETS@ --output results.txt
# All @SECRETS@ tokens are replaced with /dev/fd/9

Export Mode (Environment Variable Export)

For tools that expect secrets as actual environment variables (like Ansible, shell scripts, or tools that call os.getenv), use the --export flag:

kleys --export ansible-playbook site.yml

How it works:

  • Before running your command, kleys parses the secrets into KEY=VALUE pairs
  • These pairs are injected into the subprocess environment — your command sees them as real env vars
  • No temp file is written — secrets are loaded directly from keyring into memory
  • Works with @SECRETS@ too — exports from keyring directly into env without touching disk
  • When a local .env file already exists (not loaded from keyring), it is read directly from disk

Which tools benefit from --export?

Tool Without --export With --export
Ansible source .env && ansible-playbook ... kleys --export ansible-playbook ...
Terraform source .env && terraform plan kleys --export terraform plan
Shell scripts source .env && ./deploy.sh kleys --export ./deploy.sh
Any os.getenv/$VAR consumer needs vars in environment vars are exported automatically

Key difference: without --export, secrets are written to a temp file and SECRETS_FILE env var is set. With --export, secrets are loaded directly into memory — no temp file, no SECRETS_FILE, just real env vars.

Combined with @SECRETS@:

kleys --export ansible-playbook --vault-password-file @SECRETS@ site.yml

This both exports secrets into the environment AND passes one via file descriptor — maximum flexibility with zero disk writes.

Encrypted Mode (Default, Password-Protected Secrets)

Encryption is enabled by default. All secrets are encrypted with Fernet (AES-128-CBC + HMAC-SHA256) before being stored in the keyring:

# Default: prompts for password (with confirmation) on first use
kleys npm start

# Password from environment variable
KLEYS_PASSWORD=hunter2 kleys npm start

# Explicit password (visible in ps — use with care)
kleys --password hunter2 npm start

# Opt out of encryption
kleys --unencrypted npm start

How it works:

  1. Encrypted entries are stored under a separate keyring key: app_name-encrypted (distinct from the plaintext key app_name).
  2. On lookup, the tool tries the encrypted key first. If found, it resolves the password and decrypts.
  3. On first run (no existing entry), you'll be prompted for a password (with confirmation) unless KLEYS_PASSWORD or --password VALUE is set.
  4. Existing plaintext entries remain readable with a warning: ℹ Found plaintext entry — not encrypted. New entries will be encrypted.
  5. Use --unencrypted to disable encryption entirely (e.g., for CI/CD scripts that can't provide a password).

Password resolution priority (encrypt and decrypt):

Priority Source
1 --password VALUE (explicit)
2 KLEYS_PASSWORD env var
3 Interactive prompt (with confirmation when storing)

Password confirmation: When prompted interactively to create a new encrypted entry, the password is asked twice to prevent typos. The decrypt path (loading existing entries) prompts once without confirmation.

Auto-detection: If an encrypted entry exists and no password flags are passed, the tool resolves via env var or prompt automatically.

Security: Encrypted secrets resist D-Bus GetSecret attacks — an attacker who enumerates the keyring gets ciphertext, not plaintext. The decryption key is never stored in the keyring.

Key derivation: Uses PBKDF2-HMAC-SHA256 with 600,000 iterations and a random 16-byte salt for each encryption operation.

CI/CD note: If you run kleys in automation without a password, you must add --unencrypted or set KLEYS_PASSWORD:

# After (encryption is default — choose one):
kleys --unencrypted deploy.sh
# OR
KLEYS_PASSWORD=$(cat /etc/secret.txt) kleys deploy.sh

First-Run Setup

On first use (when secrets aren't in keyring):

  1. kleys prompts: "Paste your secrets content..."
  2. Paste your .env content (KEY=VALUE format)
  3. Press Ctrl-D (Unix) / Ctrl-Z + Enter (Windows) to finish (or Ctrl-C to cancel)
  4. Secrets are encrypted and stored in system keyring
  5. Future runs load automatically

Security Notes

  • Keyring encryption: Secrets stored in your system's encrypted keyring service
  • File permissions (file mode): Temporary files created with 600 permissions (owner read/write only)
  • Short-lived exposure (file mode): Files on disk exist only during command execution
  • Zero disk I/O: Use @SECRETS@ (FD mode) or --export (export mode) — secrets never touch disk
  • No git commits: No .env files left behind to commit accidentally
  • Session isolation: Each terminal session can use different secrets with --key flag
  • Encrypted payloads (Fernet/AES-128-CBC): Secret content is encrypted with Fernet (AES-128-CBC + HMAC-SHA256) before keyring storage by default, protecting against D-Bus GetSecret enumeration attacks. See the section below for details. Use --unencrypted to disable.

Why Encrypted Mode Matters: The Keyring Enumeration Attack

The Linux Secret Service API (D-Bus) that powers GNOME Keyring, KWallet, and similar services is accessible to any process running under your user account — no authentication required. This means any code on your machine (malware, a compromised pip install, a malicious Node.js package, or even a curious colleague) can enumerate every item in your keyring:

import secretstorage
bus = secretstorage.dbus_init()
col = secretstorage.get_default_collection(bus)
for item in col.get_all_items():
    print(f'  Label: {item.get_label()}')
    print(f'  Attributes: {item.get_attributes()}')
    print(f'  Secret: {item.get_secret().decode(errors="replace")}')
    print()

This is not a vulnerability in the keyring — it is by design. The keyring service provides session-level isolation (secrets are encrypted at rest when the session is locked), but once you unlock your keyring at login, any process sharing your D-Bus session can retrieve every secret in plaintext.

This is why kleys encrypts by default. When using encrypted mode (Fernet/AES-128-CBC, enabled by default):

  • An enumerating attacker sees only ciphertext — meaningless without the decryption password
  • The decryption password is never stored in the keyring (resolved via interactive prompt, environment variable, or --password flag)
  • Even if an attacker dumps every keyring entry, your secrets remain confidential

See Security Architecture for a detailed analysis of the D-Bus attack surface and encryption protocol.

When --unencrypted is used, secrets in the keyring are as exposed as .env files on disk — any process with D-Bus access can read them. Reserve --unencrypted for ephemeral or isolated environments (e.g., CI containers where D-Bus access is restricted).

⚠️ Important: While kleys improves security, temporary files are still written to disk briefly in file mode. For maximum security:

  • Use @SECRETS@ for tools that accept a file path (FD mode — zero disk I/O)
  • Use --export for tools that need env vars (export mode — zero disk I/O)
  • Use encrypted home directories
  • Ensure your keyring is properly locked when not in use
  • Be cautious running kleys on shared systems

Troubleshooting

"Command fails with @SECRETS@"

The command may require a regular file instead of a file descriptor. Try without the @SECRETS@ token:

# If this fails:
kleys mycommand --file @SECRETS@

# Try this instead:
kleys mycommand

"No secrets found" on every run

The keyring store may have failed silently. Run with --unencrypted and --export to trigger a fresh prompt:

kleys --unencrypted --export your-command

Command fails but secrets file remains

If your command crashes before cleanup runs, manually remove the temp file:

rm .env       # on Unix/macOS
del .env      # on Windows (cmd.exe)
Remove-Item .env  # on Windows (PowerShell)

Want to delete stored secrets

Use the clear subcommand to delete all stored secrets for a key:

kleys clear
kleys clear --key myproject

Uninstallation

pip uninstall kleys

Or with the tool installer you used:

uv tool uninstall kleys  # if installed with uv
pipx uninstall kleys     # if installed with pipx

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

kleys-0.1.0.tar.gz (2.3 MB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

kleys-0.1.0-py3-none-any.whl (29.4 kB view details)

Uploaded Python 3

File details

Details for the file kleys-0.1.0.tar.gz.

File metadata

  • Download URL: kleys-0.1.0.tar.gz
  • Upload date:
  • Size: 2.3 MB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.13

File hashes

Hashes for kleys-0.1.0.tar.gz
Algorithm Hash digest
SHA256 af5a532575bfccfa5f78363e5d58eb62785472f0f97ed4002b2ac57794cdee27
MD5 91defcfd2102449dbd1367bef34a43b7
BLAKE2b-256 8ac06bd8937159d6589e0de901a1945285b8e3a2ab69ad157cb288805c6df77a

See more details on using hashes here.

Provenance

The following attestation bundles were made for kleys-0.1.0.tar.gz:

Publisher: pypi.yml on hugobatista/kleys

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file kleys-0.1.0-py3-none-any.whl.

File metadata

  • Download URL: kleys-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 29.4 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.13

File hashes

Hashes for kleys-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 4f7e15ad85fb94d114ddb8d47536bac775e942fca41ad0f98a75290defa64e8a
MD5 89e97dd5a72662a4192d1da09df35f45
BLAKE2b-256 6f83248849d49b8679a2faa75ce3b3df00451d6c962016300122cdf9b3163585

See more details on using hashes here.

Provenance

The following attestation bundles were made for kleys-0.1.0-py3-none-any.whl:

Publisher: pypi.yml on hugobatista/kleys

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page