CLI Reference
Unified secrets CLI.
`hasp` dispatches `get` / `put` / `list` / `delete` / `exists` to
multiple backends addressed by URL or alias.
Usage: hasp <COMMAND>
Commands:
get Fetch a secret
put Store a secret
list List entries matching a URL prefix or alias
delete Delete a secret
exists Check whether a secret exists
cp Copy a secret from one URL or alias to another
run Run a command with secrets injected as environment variables
init Create a starter profiles.toml
help Print this message or the help of the given subcommand(s)
Options:
-h, --help Print help (see a summary with '-h')
Exit codes
hasp uses a granular exit-code convention so scripts can distinguish
failure modes without parsing stderr.
| Code | Meaning |
|---|---|
| 0 | Success. |
| 1 | Usage or local error (bad flags, malformed URL, unknown scheme, unsupported operation, IO error). |
| 2 | Not found (the URL is well-formed; the secret does not exist). |
| 3 | Permission denied (the caller’s credentials are valid but lack access). |
| 4 | Transport / network failure (transient or throttled). Retry may help. |
| 5 | Authentication failed (credentials missing, invalid, or expired). |
| 6 | Precondition failed (e.g. cp cross-environment refusal, plain-http proxy refusal, --verify mismatch, --if-exists=fail blocked). |
| 7 | Backend failure (permanent / unexpected backend response that doesn’t fit a more-specific code). |
hasp exists is a special case: it overloads codes 0 and 1 to mean
present and absent. Backend errors during exists still flow
through the table above (auth=5, transport=4, …).
hasp get <address>
Fetch a secret and print its value to stdout.
hasp get env://HOME
hasp get file:///etc/secrets/db-password
hasp get @prod/db_password
- Stdout: The secret value, verbatim.
- Stderr: Errors, hints, and warnings only.
- Exit codes: See Exit codes above.
-F / --field <path> — field extraction
For backends that store JSON payloads (vault://, aws-sm://,
gcp-sm://, azure-kv://), extract a single scalar from the payload
inside the backend, before the value crosses the SecretString
boundary. Avoids piping plaintext through jq (which leaks the parent
payload via pipe buffers and shell history).
hasp get -F password vault://kv/data/app/db
hasp get -F .credentials.api_key aws-sm://us-east-1/myapp
hasp get "aws-sm://us-east-1/myapp?field=.credentials.api_key"
<path>accepts both flat keys (password) and dotted nested paths (.credentials.api_key). Leading.is optional.-Fis sugar for the URL query parameter?field=<path>. Both forms on the same invocation are refused (exit code 1).- Non-JSON payloads fail with
invalid URL for backend: secret is not JSON. Missing keys fail withnot found: field '...' not found(exit code 2). Non-scalar leaves (objects, arrays, null) fail withinvalid URL(exit code 1).
hasp get --explain <address>
Preview which backend will handle the address and whether the result is already cached, without actually fetching the secret.
hasp get --explain env://HOME
# scheme=env backend=env cached=false
hasp put <address> [<value>]
Store a secret.
hasp put file:///tmp/secret "my-value"
hasp put env://NEW_VAR "value" # unsupported — env is read-only
hasp put file:///tmp/secret - # read from stdin
hasp put file:///tmp/secret # prompt securely in TTY
- Arguments:
address— URL or alias of the secret.value— Value to store. Use-for stdin. Omit in a TTY to prompt securely viarpassword.
hasp list <address>
List entries matching a URL prefix or alias.
hasp list vault://127.0.0.1/secret/
hasp list --format json vault://127.0.0.1/secret/ | jq '.[].name'
- Arguments:
address— URL or alias of the collection to list.--format— Output style:plain(default) —name url, two-space separatedtable— aligned columns, human-readablejson— compact JSON array of{"name": "...", "url": "..."}objects
- Output: Lines of entries, or an empty result when none match.
hasp delete <address>
Delete a secret.
hasp delete file:///tmp/secret
hasp delete keyring://my-service/admin
hasp exists <address>
Check whether a secret exists.
hasp exists env://HOME && echo "present" || echo "missing"
- Exit code: 0 if present, 1 if absent. Backend errors (auth, transport, etc.) use the standard table from Exit codes.
hasp cp <src> <dst>
Copy a secret from one backend to another. cp is the only verb that
reads and writes a secret in a single invocation, so its security
model is documented inline below — read it before scripting production
migrations.
hasp cp file:///tmp/old.txt file:///tmp/new.txt
hasp cp env://OLD_NAME file:///etc/secrets/new
hasp cp @stage/db @prod/db --yes --verify
hasp cp --explain @stage/db @prod/db # dry-run; resolves both
# backends, prints plan,
# does not read or write
- Arguments:
src— Source URL or alias. Backend must supportget.dst— Destination URL or alias. Backend must supportput.
- Flags:
--if-exists <fail|overwrite|skip>— Disposition whendstalready holds a value. Defaultfail.-f, --force— Shorthand for--if-exists=overwrite.--verify— Re-readdstafter writing and constant-time compare against the source. Mismatch returns a precondition-failed exit with no byte-level information.-y, --yes— Confirm a cross-environment copy (see “Security model” below).--explain(global) — Treated as dry-run forcp: resolves both URLs and prints the plan; does not callgetorput.
- Exit codes: Refusals (cross-environment, plain-http proxy,
self-copy,
--verifymismatch,--if-exists=failblocked) return code 6 (precondition). See Exit codes.
Security model
The defaults are deliberately stricter than Unix cp:
--if-exists=failis the default. Silent clobbering of a production secret with a staging value is materially worse than a non-zero exit demanding--force. Pass--force(or--if-exists=overwrite) to opt in.- Self-copy refused.
hasp cp file:///x file:///xreturns an error. Prevents version-counter inflation on backends that version writes (AWS Secrets Manager, Azure Key Vault, GCP Secret Manager). - Cross-environment refusal. When both
srcanddstare profile aliases AND both profiles declare anenvironment = "..."key inprofiles.toml, a mismatch refuses without--yes. Absent labels disable the check (backwards-compatible). - Plain-http proxy refusal. When
HTTP_PROXY/HTTPS_PROXY/--proxy-urlresolves to anhttp://URL,cprefuses unlessHASP_ALLOW_HTTP_PROXY=1is set. The doubled-exposure window ofcpmakes MITM more costly than for other verbs. - Audit events.
cp.startandcp.doneare emitted by the library; see Audit events for the wire format and how to redirect or disable them. --verifyuses constant-time comparison viasubtle::ConstantTimeEq. A failed verify returns a generic “verify failed: source and destination differ” message with no byte-level diff.- No atomicity across backends. A failed
put(dst)after a successfulget(src)leavesdstin an indeterminate state — either untouched or partially written, depending on the dst backend’s semantics.haspcannot promise two-phase commit across heterogeneous stores. cpcopies the value, not the access policy. Copying from a tightly-controlled store to a loosely-controlled one effectively widens access to the value.hasphas no view into either backend’s IAM model.
For the full threat model and platform-hardening rationale, see
docs/internal/research/RESEARCH-cp-threat-model.md.
hasp diff <a> <b>
Compare two secrets across (possibly different) backends without revealing anything about their contents beyond a binary match/differ verdict.
hasp diff @stage/db @prod/db # exits 0 on match, 1 on differ
hasp diff file:///tmp/a file:///tmp/b
hasp diff --explain @stage/db @prod/db # dry-run; no fetch
- Arguments:
a,b— URLs or aliases. Both backends must supportget.
- Flags:
-y, --yes— Confirm a cross-environment comparison (mirrorscp).--explain(global) — Resolves both URLs and prints the plan; does not callget.
- Exit codes:
0— Both secrets compared byte-equal.1— Secrets differ (length or content), OR a usage error (parallelshasp exists). Backend errors flow through the standard Exit codes table — auth=5, transport=4, etc.
- Security: the verdict is binary by construction. Mismatch never
reveals byte counts, common prefixes, diff positions, or hashes. The
compare path uses
subtle::ConstantTimeEq(the same cratecp --verifyuses) so timing channels are not informative about a near-match. Both secret bodies stay insideSecretStringend-to-end and are dropped beforediffreturns. - Audit events:
diff.start/diff.donewith outcome"match"/"differ"/"error". See Audit events. - Cross-environment refusal: same shape as
cp. Aliases whose profiles declare mismatchedenvironment = "..."labels require--yes; the refusal is not about safety (diff doesn’t write) but about pulling secrets from two trust tiers into the same process. - Plain-http proxy refusal: identical to
cp; a MITM on the proxy can still observe the values pulled through it.
hasp run -e KEY=URL [...] -- <cmd> [args...]
Fetch secrets by URL and inject them as environment variables into a child process. The child inherits hasp’s exit code verbatim.
hasp run -e DB_PASS=aws-sm://us-east-1/prod/db \
-e API_TOKEN=vault://kv/data/app/api?field=token \
-- ./my-app --serve
# Mix schemes; field extraction from #5 works here too
hasp run -e TOKEN=aws-sm://us-east-1/prod/creds?field=api_key \
-- ./service
-e KEY=URLis repeatable. Each URL is resolved through the configuredStore;-e DB_PASS=@prod/dbalso works (profile alias).- All-or-nothing: if any fetch fails, the child is never spawned and hasp returns the appropriate exit code (e.g. 2 for not-found).
- Duplicate keys are refused at startup (exit code 1).
- TTY refusal: neither stdout nor stderr may be a terminal when
invoking
run— accidental use likehasp run -- echo $DB_PASSwould expose secrets in the terminal scroll buffer regardless of which stream the child writes to. Pass--allow-ttyto override for interactive debugging. /proc/<pid>/environvisibility: on Linux, same-uid processes can read a child’s environment via/proc/<pid>/environ. This is the inherent cost of env injection. For the highest-isolation workloads, consider named-pipe or tmpfs delivery instead.- PTY masking (1Password’s
op runfeature — intercepts the child’s stdout to mask accidentally echoed secrets) is not yet implemented; scheduled as a follow-up. - Audit events:
run.startandrun.doneare emitted; each intermediategetemits its ownget.start/get.doneevents.
hasp init
Create a starter profiles.toml in the platform config directory.
hasp init
# Refuse to overwrite an existing file:
hasp init --force
- Respects
HASP_PROFILES_PATHif set. Refuses to overwrite an existing file without--force(exit code 1).
hasp complete <shell>
Generate a static completion script for the requested shell. Hidden
from --help.
hasp complete bash
hasp complete zsh
hasp complete fish
hasp complete powershell
hasp man
Generate a man page in ROFF format. Hidden from --help.
hasp man > /usr/share/man/man1/hasp.1
Global flags
| Flag | Description |
|---|---|
-h, --help | Print help. Pass -h for a summary, --help for full help. |
-q, --quiet | Suppress non-error informational output. |
-v, --verbose | Increase output verbosity; prints operation traces to stderr. Can be used multiple times (-vv). |
--no-cache | Disable the per-invocation in-process secret cache for this invocation. |
--no-profile-allow | Skip the profile-allow enforcement for this invocation (per-invocation opt-out of the default-on HASP_REQUIRE_PROFILE_ALLOW enforcement). |
Profile-allow enforcement
Profile-allow is on by default. Before hasp will use profile
aliases (@profile/key), the operator must run hasp profile allow
to record profiles.toml as trusted (mtime + SHA-256 baseline,
direnv-style).
Opt out per environment with HASP_REQUIRE_PROFILE_ALLOW=0 (also
accepts false, no, off). Per-invocation bypass via
--no-profile-allow.
Refusal exit code is 6 (precondition) — matches the verb-error
mapping convention; scripted callers can distinguish “no trusted
profiles.toml” from usage errors (1).
Caching
hasp memoizes fetched secrets for the lifetime of a single invocation
(5-minute TTL, 1024-entry capacity ceiling). This eliminates the
duplicate-URL footgun when a script issues several hasp get calls for
the same URL in one invocation or uses Store::batch_get.
The cache lives only in process memory. There is no on-disk
persistence, no daemon, no IPC. Cache lifetime ≤ process lifetime by
construction. Cached entries hold Arc<SecretString> references whose
inner heap buffer zeroizes on eviction.
Disabling the cache
The cache is disabled when any of the following is true:
--no-cacheis passed on the command line.HASP_NO_CACHE=1is set in the environment (also acceptstrue,yes,on).- The
CIenvironment variable is set. CI runners are the documented target of credential-cache-targeting supply-chain worms (Bitwarden CLI 2026.4.0 compromise; Mini Shai-Hulud / CanisterWorm worms in May 2026). Auto-disabling defends against the warm-cache exfil class without forcing every CI pipeline to remember the flag.
Configuring the TTL envelope
HASP_CACHE_TTL=<seconds> overrides the default cache TTL. Valid
range: 1..=3600. Values above 3600 are clamped to AWS Secrets
Manager Agent’s published 1-hour ceiling. HASP_CACHE_TTL=0 disables
the cache entirely (equivalent to --no-cache).
Clearing the cache
hasp cache clear
Drops every cached entry within the current invocation. (The in-process
cache lives only for the current process, so the gesture is primarily
useful as a no-op exit; once the cache-persistent on-disk variant
ships, the same command will also remove the encrypted cache file and
the OS-keyring entry holding its symmetric key.)
Persistent cache (cache-persistent Cargo feature, not yet implemented)
The cache-persistent Cargo feature is the opt-in cross-invocation
encrypted-file cache, currently scaffolded only. When the
implementation lands, the file will be at
$XDG_CACHE_HOME/hasp/cache.bin (mode 0o600), encrypted with
XChaCha20-Poly1305 using a per-host symmetric key bound to the OS
keyring (Secret Service / Keychain / Credential Manager).
The verbatim threat-model warning from the AWS Secrets Manager Agent applies and is reproduced here to set expectations:
After the secret value is pulled into the cache, any user with access to the compute environment can access the secret from the cache.
The feature is off by default for a reason: any persistent cache file inherits the threat surface that infostealers and supply-chain worms target by name (Bitwarden CLI 2026.4.0; Mini Shai-Hulud / CanisterWorm, May 2026). Enable only after considering the deployment threat model.
Cache audit events
Each cache decision emits a structured one-line JSON event via the
configured AuditSink:
{"event":"cache.miss","ts":1747000000,"src_scheme":"vault","outcome":"miss"}
{"event":"cache.hit","ts":1747000001,"src_scheme":"vault","outcome":"hit"}
{"event":"cache.expire","ts":1747000300,"src_scheme":"vault","outcome":"expire"}
{"event":"cache.clear","ts":1747000400,"src_scheme":"all","outcome":"clear"}
The closed-shape CacheEvent enum prevents value bytes from ever
landing in the audit stream; the proptest in
crates/hasp-core/tests/audit_no_leak.rs enforces this.
Threat model
The cache only protects against re-fetching from the backend. It does not protect against:
/proc/<pid>/meminspection by a same-uid attacker (PR_SET_DUMPABLE=0mitigates this; the hardening token witnesses that the mitigation has been applied before any cache is constructed).- A coredump triggered after a cache hit (mitigated by
RLIMIT_CORE=0, also required by the hardening token). - A debugger attached by the same user before cache construction.
For cross-invocation persistence (the encrypted-file-on-disk variant),
see the cache-persistent Cargo feature in the next sprint slot.
Audit events
Every hasp verb emits structured *.start / *.done JSON events to
stderr (one line each). The wire format:
{"event":"get.done","ts":1747612345,"src_scheme":"vault","outcome":"ok"}
{"event":"cp.done","ts":1747612346,"src_scheme":"vault","dst_scheme":"file","outcome":"copied"}
{"event":"get.done","ts":1747612347,"src_scheme":"env","outcome":"error","error_kind":"not_found"}
Fields: event (closed set), ts (UNIX seconds), src_scheme,
dst_scheme (cp/run only), outcome, error_kind (on failure).
No values, no lengths, no value-derived material are ever emitted.
Controlling the sink
Resolution order (highest precedence first):
HASP_AUDITenv var.~/.config/hasp/audit.toml(override location withHASP_AUDIT_CONFIG_PATH).- Default: stderr.
Env vars:
| Env var | Value | Behaviour |
|---|---|---|
HASP_AUDIT | (unset) | Use audit.toml if present, else stderr |
HASP_AUDIT | off | Suppress all audit output |
HASP_AUDIT | stderr | Write JSON lines to stderr |
HASP_AUDIT | file | Write to HASP_AUDIT_PATH (falls back to stderr if path is empty, NoopSink if open fails) |
HASP_AUDIT | syslog | Forward to local syslog daemon (Unix only); ident from HASP_AUDIT_IDENT (default "hasp"). Falls back to stderr on Windows. |
HASP_AUDIT_PATH | /path/to/audit.log | Log file for file mode (0600 on Unix) |
HASP_AUDIT_IDENT | "hasp" | Program ident shown in syslog entries (syslog mode) |
HASP_AUDIT_CONFIG_PATH | /path/to/audit.toml | Override the default audit.toml location |
Example ~/.config/hasp/audit.toml:
[audit]
sink = "file"
path = "/var/log/hasp/audit.log"
# Or:
# [audit]
# sink = "syslog"
# ident = "hasp-prod"
Library consumers: pass an Arc<dyn hasp::AuditSink> to
StoreBuilder::with_audit_sink(...) to install a custom sink.
SyslogSink is gated on #[cfg(unix)] and wraps the libc syslog
client (openlog / syslog / closelog) so it picks up the
platform’s native socket path (/dev/log on Linux,
/var/run/syslog on macOS) without a third-party dependency.
Threat model
The audit stream documents that an access happened — it is not a forensic guarantee that one will happen for every secret retrieval. Trust boundary:
- Same-uid tampering. A process running as the same uid as
haspcan redirect or suppress the audit stream — for example by settingHASP_AUDIT=offbefore invokinghasp, by truncating the log file an earlier invocation wrote, or by overwriting the binary. Treat the stream as best-effort telemetry from a cooperating caller, not as a tamper-evident security log. For tamper-evident logging, ship the stream off-host (e.g. syslog forwarding to a write-once collector) or runhaspin a privilege-separated environment. - Concurrent writers.
FileSinkserializes writes via an internalMutex<File>; concurrent invocations ofhaspwriting to the same path will not interleave bytes within a single process but two separate hasp processes appending to the same path may produce events out of timestamp order. Sort bytson ingest. - No value leakage.
AuditEvent’s field set is closed (#[non_exhaustive]) and every field is either a timestamp, a&'static strfrom a closed set, or a URL scheme. No implementation ofAuditSinkcan leak a value or a value-derived length.
Environment variables
| Variable | Effect |
|---|---|
HASP_PROFILES_PATH | Override the default profiles.toml path. |
HASP_ALLOW_HTTP_PROXY | Set to 1 to allow hasp cp through a plain-http proxy. |
HASP_AUDIT | Audit sink mode: unset=stderr (or audit.toml), off=silent, file=log file, syslog=local syslog daemon (Unix). |
HASP_AUDIT_PATH | Path for HASP_AUDIT=file mode (append, 0600 on Unix). |
HASP_AUDIT_IDENT | Program ident for HASP_AUDIT=syslog mode (default "hasp"). |
HASP_AUDIT_CONFIG_PATH | Override ~/.config/hasp/audit.toml location. |
Address argument
The address positional argument accepts:
- A full URL:
env://VAR,file:///path,aws-sm://region/name - A profile alias:
@profile/keyor@profile(when a self-key exists)
Tab completion is available for URL schemes, profile aliases, and
file:// paths. See Shell Completions.