cage

command module
v0.0.9 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jun 22, 2026 License: MIT Imports: 4 Imported by: 0

README

cage

cage is a minimal, opinionated, macOS-only CLI for loading 1Password Environments with age-protected 1Password service account tokens.

It is a different take on tools like fnox: configuration is global, profile/environment resolution is flat, and secrets are fetched only for the command being run.

Repository: https://github.com/2h2d-co/cage

Features

Feature set:

  • Global config only: $XDG_CONFIG_HOME/cage/config.toml, falling back to ~/.config/cage/config.toml
  • Development/testing override: CAGE_CONFIG=PATH or --config PATH
  • macOS only
  • native age identity creation/listing/deletion from cage config
  • age Go library with plugin protocol support
  • age-plugin-yubikey identity creation/listing/deletion from cage config
  • age-plugin-se identity creation/listing/deletion from cage config
  • encrypted 1Password service account providers as NAME.1p.age
  • non-secret identity, provider, Environment, and profile listing/inspection
  • 1Password Environment and profile config management
  • cage get and cage exec
  • encrypted environment cache inspection, pruning, clearing, and launchd scheduling
  • read-only cage doctor diagnostics for config, local files, and cache state
  • shell completions and manpage generation

Unsupported: TUI and hierarchical config.

Requirements

  • macOS
  • Go 1.26+
  • age-plugin-yubikey for YubiKey identities
  • age-plugin-se for Secure Enclave identities

Install hints used by cage:

brew install age
brew install age-plugin-yubikey age-plugin-se # if you use those identity types

Install

Release artifacts are Apple Silicon-only (darwin_arm64). Homebrew, mise, and manual tarball installs require an Apple Silicon Mac. go install builds from source, but cage is only supported on macOS.

Homebrew
brew install 2h2d-co/tap/cage
mise
mise use -g github:2h2d-co/cage@0.0.9
# or in a project mise.toml:
# "github:2h2d-co/cage" = "0.0.9"
Manual tarball
curl -L https://github.com/2h2d-co/cage/releases/download/v0.0.9/cage_0.0.9_darwin_arm64.tar.gz -o cage_0.0.9_darwin_arm64.tar.gz
tar -xzf cage_0.0.9_darwin_arm64.tar.gz
install -d "$HOME/.local/bin"
install -m 0755 cage "$HOME/.local/bin/cage"

The GitHub release publishes a darwin_arm64 archive with the cage binary at the archive root, plus checksums and GitHub artifact attestations for mise's github: backend.

Go install
go install github.com/2h2d-co/cage@v0.0.9
Local development
mise install
make test
make build

With mise shell integration active, cage:local is an alias for go run ., and cage:it runs local cage with CAGE_CONFIG=${XDG_CONFIG_HOME:-$HOME/.config}/cage/integration-test/config.toml.

Config

The config file is $XDG_CONFIG_HOME/cage/config.toml, falling back to ~/.config/cage/config.toml when XDG_CONFIG_HOME is unset. Set CAGE_CONFIG=PATH to use another config, or pass --config PATH to override both. Identity and provider file paths must be relative paths that stay within the config file directory.

Example:

[identities]
local = { type = "basic", file = "local.identity" }
work1 = { type = "yubikey", file = "work1.identity" }
work2 = { type = "secure-enclave", file = "work2.identity" }

[providers]
project1 = { type = "1password", identity = "local", file = "project1.1p.age" }
project2 = { type = "1password", identity = "work2", file = "project2.1p.age" }

[environments]
dev = { type = "1password-environment", provider = "project1", uuid = "00000000-0000-0000-0000-000000000000", cache = { ttl = "15m", identity = "local" } }
stage = { type = "1password-environment", provider = "project2", uuid = "11111111-1111-1111-1111-111111111111" }

[profiles]
default = ["dev"]
proj2-prod = ["dev", "stage"]

Supported types:

  • identities: basic, yubikey, secure-enclave
  • providers: 1password
  • environments: 1password-environment

Profiles can only reference environments. They cannot reference other profiles.

Environment caches are optional. Add cache = { ttl = "15m", identity = "local" } to an environment to cache that Environment for a positive Go duration string encrypted to the named identity. Changing ttl caps already-written entries by the current config TTL. Cached Environment files are stored under ${XDG_CACHE_HOME:-$HOME/.cache}/cage/environments/; cache state is tracked in ${XDG_STATE_HOME:-$HOME/.local/state}/cage/cage.db.

Identity management

Created identity names must use only letters, numbers, _, and -. Created files are always named NAME.identity and written with mode 0600.

cage identity list
cage identity basic create local
cage identity basic create local-pq --pq
cage identity basic list
cage identity basic delete local

cage identity yubikey create work1
cage identity yubikey list
cage identity yubikey list --connected
cage identity yubikey delete work1

cage identity se create work2
cage identity se list
cage identity se delete work2

Basic identities are native age X25519 identities by default; pass --pq to generate a native post-quantum age identity. YubiKey options include --serial, --slot, --pin-policy, --touch-policy, and --force-slot. Secure Enclave options include --access-control, --recipient-type, and --pq; --access-control defaults to any-biometry.

delete removes the cage config entry and local identity file. age-plugin-yubikey does not expose key-material deletion, so YubiKey key material is not erased by cage.

1Password providers

Create and inspect encrypted 1Password service account providers:

cage provider list
cage provider 1p create project1 --identity local

If stdin is not a terminal, cage reads the token from stdin. You can also force stdin:

printf '%s' "$OP_SERVICE_ACCOUNT_TOKEN" | cage provider 1p create project1 --identity local --stdin

The plaintext token is encrypted only to the configured identity and stored as project1.1p.age with mode 0600. cage provider list shows metadata such as referenced identities and provider file status without decrypting tokens. cage does not expose provider tokens to child processes.

Environment and profile management

Create, list, inspect, and delete 1Password Environment config entries:

cage environment create dev --provider project1 --uuid 00000000-0000-0000-0000-000000000000
cage environment create dev-cached --provider project1 --uuid 00000000-0000-0000-0000-000000000000 --cache-ttl 15m --cache-identity local
cage environment cache set dev --ttl 15m --identity local
cage environment cache set dev --ttl 1h --identity local --overwrite
cage environment cache unset dev
cage environment list
cage environment inspect dev
cage environment delete dev

Create, list, and delete flat profiles:

cage profile create default --environments dev,stage
cage profile list
cage profile delete default

environment cache set adds cache settings and requires both --ttl and --identity. If an environment already has cache settings, pass --overwrite with both settings to replace them. environment cache unset removes cache settings from the config; use cage cache clear NAME to remove existing encrypted cache data.

list and inspect management commands show metadata and wiring only, never resolved secret values. Environment deletion is blocked while a profile still references that environment. Recreate or delete the profile first.

Resolution rules

--profiles and --environments accept comma-separated names.

  • Flags override CAGE_PROFILES and CAGE_ENVIRONMENTS.
  • Profiles load first, in the given order.
  • Environments from --environments load after profiles, in the given order.
  • Last loaded environment variable wins.
  • There is no default profile; selecting no profile/environment is an error.

If an environment has encrypted caching configured, get and exec use an active cache before pulling from 1Password. --skip-cache avoids both cache reads and writes. --refresh-cache pulls fresh values and updates configured caches. If a cache entry is unreadable, cage deletes it and pulls fresh values.

Cache management

Inspect encrypted Environment cache metadata without printing secret values:

cage cache list
cage cache list --json
cage cache status dev
cage cache status dev --json

Remove cache entries that cannot be used because they are expired, inactive, missing, or unreadable:

cage cache prune

Clear encrypted cache data for one environment or for the current config:

cage cache clear dev
cage cache clear --all

Install a per-user macOS launchd LaunchAgent that periodically runs cage cache prune with the current executable path and config path:

cage cache launchd install
cage cache launchd install --interval 30m --overwrite

The LaunchAgent plist is written to ~/Library/LaunchAgents/co.2h2d.cage.cache-prune.plist. Logs are written to ~/Library/Logs/co.2h2d.cage.cache-prune.log and ~/Library/Logs/co.2h2d.cage.cache-prune-error.log. If XDG_CACHE_HOME or XDG_STATE_HOME is set during installation, those values are preserved in the LaunchAgent. For advanced parallel/testing setups, set CAGE_CACHE_PRUNE_LAUNCHD_LABEL to use a different launchd label and derived plist/log names.

Uninstall the LaunchAgent:

cage cache launchd uninstall

Diagnostics

Run read-only diagnostics for config parsing, references, file ownership/permissions, identity/provider metadata, and encrypted cache state:

cage doctor
cage doctor --json
cage doctor --strict

doctor does not decrypt provider tokens, decrypt cached Environment payloads, contact 1Password, prompt for hardware-backed identities, or prune cache files. It reports metadata only. By default warnings exit 0 and failures exit 1; --strict exits non-zero when warnings are present.

Get

Print one value:

cage get --profiles default DATABASE_URL

Print all values as dotenv-style lines:

cage get --profiles default '*'

Use this form for quick inspection or simple single-line values. Values are not shell-quoted; multiline values span multiple output lines and are not robust for parsing. Prefer JSON for multiline secrets or machine-readable output.

Print JSON:

cage get --profiles default --json '*'

Bypass configured caches:

cage get --profiles default --skip-cache '*'
cage get --profiles default --refresh-cache '*'

Missing variables print an error and exit with status 1.

Exec

Run a command with parent environment plus resolved cage variables:

cage exec --profiles default -- npm run start
cage exec --profiles default --refresh-cache -- npm run start

On macOS, cage uses exec to replace itself with the child process. OP_SERVICE_ACCOUNT_TOKEN is removed from the child environment even if present in the parent environment or loaded environment.

Integration tests

An optional end-to-end integration suite lives in integration/. It requires a local integration-test cage profile, identity, encrypted provider file, and 1Password Environments; no secret material is checked into this repository. The local setup can use the mise shell alias cage:it.

mise run integration:run:all

See integration/README.md for setup instructions.

Shell completions and manpages

cage completion zsh > _cage
cage completion bash > cage.bash
cage completion fish > cage.fish

cage man ./man
man ./man/cage.1

A checked-in starter manpage is also available at docs/man/cage.1.

Security/UX notes

  • Provider tokens are decrypted only in memory to initialize the 1Password SDK.
  • 1Password Environments are not cached in memory across environment loads. Optional on-disk Environment caches are always age-encrypted to the configured cache identity.
  • Expired, inactive, unreadable, and replaced cache files are removed with normal file deletion; cache secrecy relies on age encryption, not overwrite passes.
  • Config directories, config files, identity files, provider files, cache directories, cached Environment files, and the cache state database must be owned by the current user and not accessible by group or others.
  • Cage strips common credential, injection, and debug environment variables from age plugin subprocesses.
  • Errors are redacted for common secret-looking values before cage prints them.
  • Use --verbose for high-level diagnostics or --debug for extra details; secret values are not intentionally logged.

License

MIT © Two Humans and Two Dogs LLC (2h2d.co)

Documentation

Overview

Package main contains the cage command-line entry point.

Directories

Path Synopsis
internal
cage
Package cage implements the cage CLI command tree and runtime behavior.
Package cage implements the cage CLI command tree and runtime behavior.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL