Once: Cache CLI commands
Once: Cache CLI commands
once is a utility designed to execute a command in your current working directory, output the results to stdout, and store that output in a lightweight, per-user background daemon.
If you call the same command again—provided the directory and tenant key remain the same—the tool retrieves the result from memory instead of re-running the process, until the cache entry expires.
🎯 Primary Use Case
The most common application is retrieving secrets from managers like 1Password. Instead of providing a fingerprint for every single read request, you can cache the token for a few hours.
Example: .zshrc Configuration
# Assign a unique tenant key for the current terminal session
export ONCE_TENANT="${ONCE_TENANT :- $(uuidgen)}"
# Cache a GitHub token for 8 hours, ignoring the directory
export GITHUB_TOKEN="$(once --ttl 8h --no-dir -- op read op://Private/GitHub/token)"
In this scenario, only the first request triggers the fingerprint prompt; subsequent requests within the 8-hour window are instant.
Example: mise Integration
Within a mise.local.toml for a specific project:
[env]
ONCE_TENANT = "5D0F399A-5091-43F1-9C43-7C5A2377D90D" # Context-specific key
TOKEN = "{{ exec(command='once --ttl 12h --no-dir -- op read op://Private/something/token') }}"
🛠 Installation & Requirements
- Go Version: 1.27+
- Dependencies: Standard library only.
- OS Support: macOS and Linux.
Install via Go:
go install github.com/alex0ptr/once@latest
📖 Usage Guide
To see all available options, run once --help, once help, or once -h.
Command Syntax
once --ttl DURATION [--until TIME] [--tenant KEY] [--refresh] [--no-dir] -- COMMAND [ARGS...]
Management Commands
once status: Displays daemon status, the number of active entries, and the scheduled shutdown time.once clear: Wipes the current cache.
Option Reference
| Option | Description |
|---|---|
--ttl | Sets the cache duration (e.g., 1h, 30m). |
--refresh | |
--no-dir | Excludes the current working directory from the cache key calculation. |
Technical Nuances
- Environment: The tool inherits the terminal, working directory, and environment variables from your shell.
- Shell Features: Because
onceexecutes commands directly, you must wrap pipes or globs in a shell call:once --ttl 1h -- sh -c 'op item get "AWS" --format json | jq -r .fields[0].value' - I/O Handling: Only
stdoutis cached.stdinandstderrare passed through in real-time, ensuring interactive prompts still function. - Error Handling: If a command returns a non-zero exit code, the result is not cached, and the error code is passed back to the shell.
📂 Directory-Independent Caching
By default, the directory is part of the key because commands like git rev-parse HEAD vary by folder. However, for global commands (like op read), use the --no-dir flag to share one cached value across all directories.
⏰ Absolute Expiration with --until
The --until flag specifies an absolute timestamp. The entry expires at whichever comes first: the --ttl or the --until time.
Supported Formats:
2026-10-09T06:00:00+02:002026-10-09T06:00:00+02002026-10-09T06:002026-10-09(interpreted as local time)
Examples:
- Expire at 10 PM today:
once --ttl 24h --until "$(date +%F)T22:00" --no-dir -- op read ... - Expire tomorrow morning (Linux/GNU):
once --ttl 24h --until "$(date -d tomorrow +%F)T06:00" -- op read ... - Expire tomorrow morning (macOS/BSD):
once --ttl 24h --until "$(date -v+1d +%F)T06:00" -- op read ...
Note: If the
--untiltime has already passed, the command runs normally but is not cached.
🔑 Tenant Keys & Logic
The cache key is generated using the following logic:
(The directory is omitted if --no-dir is used).
What is a Tenant Key?
It is a namespace, not a security boundary. It is not intended to be secret and can be stored in config files. It serves to:
- Separate contexts (e.g., different projects or shell sessions).
- Ensure a context with a different key must fetch its own secrets.
Using ${ONCE_TENANT:-$(uuidgen)} ensures every new terminal has a unique tenant, while child processes share the same cache. Warning: Keys passed via --tenant are visible in the process list.
⚙️ How It Works
- The client computes the key and queries the daemon via a Unix socket.
- On a miss, the client runs the command in the current environment.
- Upon success, the client starts the daemon (if not running) and provides the output and expiry.
- The daemon shuts down automatically once the final entry expires.
Socket Location:
The socket is stored in $XDG_RUNTIME_DIR/once-uid/ (or a temporary directory) with permissions set to 0700 to prevent other users from connecting. The client computes the key locally, so the tenant key is never transmitted to the daemon.
🛡 Security Model
Security is enforced via Unix socket and file system permissions:
- Runtime Directory: Mode
0700 - Socket: Mode
0600
While other processes under your own user account can read the tenant key (from environment variables or configs) and thus access the cache, other system users are blocked. Memory is overwritten before being dropped on a best-effort basis.