Hasp
One tool for every secret store.
Hasp is a unified command-line interface for reading, writing, listing, and deleting secrets across multiple backends. Instead of learning a different tool for every vault — AWS Secrets Manager, HashiCorp Vault, 1Password, Bitwarden, GCP Secret Manager, Azure Key Vault, your OS keyring, local files, and environment variables — you use a single consistent syntax.
Why hasp?
Every secret store ships its own CLI: aws secretsmanager, vault kv,
op read, bw get password, gcloud secrets, az keyvault. Each has
its own auth dance, its own URL conventions, its own flags, and its
own output format. When your secrets live in three different clouds plus
a password manager plus CI env vars, you end up with a shell script full
of adapters.
Hasp trades backend-specific depth for a single consistent interface across all of them:
- One binary, one syntax.
hasp get <url>,hasp put <url>,hasp list <url>,hasp delete <url>,hasp exists <url>work the same way against every backend. - URL-addressed secrets. Every secret is a URL:
env://VAR,file:///etc/secrets/db,aws-sm://us-east-1/prod/db-password,op://vault/item/field. The scheme tells hasp which backend to use; the rest is backend-specific addressing. - Profile aliases. Define shortcuts in
~/.config/hasp/profiles.tomland type@prod/db_passwordinstead of a 60-character AWS ARN. - Ambient credentials only. Hasp never stores credentials, rotates
tokens, or bootstraps auth. It reads what your environment already
provides — IAM roles,
VAULT_TOKEN,BW_SESSION, the OS keyring, or plain env vars. - Pipe-friendly output. Secrets go to stdout; everything else goes to
stderr.
hasp get @prod/api_key | curl ... -H "Authorization:Bearer $(cat -)"just works.
When to reach for something else
Hasp is not trying to replace the native CLIs when you need their specialty features. Reach for:
aws secretsmanager— when you need policy management, rotation configuration, or cross-account replication setup.vault kv— when you need namespace admin, PKI mounts, or dynamic database credential generation.op/bw— when you need to edit attachments, manage collections/organizations, or perform bulk imports.gcloud secrets/az keyvault— when you need IAM binding management, key rotation policy, or audit log export.sops/age/cocoon— when you need to encrypt whole files or TOML/JSON/YAML documents, not just individual key-value secrets.
Hasp is for terminal-first, scriptable secret access: fetching a
config value in a deploy script, checking whether a CI secret exists
before a job starts, rotating a local dev credential between
environment and file backends, or swapping a dozen aws / vault /
op calls in a runbook for a single predictable command.
One-minute demo
# Read from the environment — zero setup
export MY_SECRET="hello-world"
hasp get env://MY_SECRET
# Write a secret to a file
hasp put file:///tmp/my-secret "shh"
hasp get file:///tmp/my-secret
# Check if a secret exists (exits 1 when missing)
hasp exists env://DEFINITELY_NOT_HERE || echo "not found"
# Define an alias so you don't type the full URL
mkdir -p ~/.config/hasp
cat > ~/.config/hasp/profiles.toml <<'EOF'
[profiles.local]
my_secret = "env://MY_SECRET"
EOF
hasp get @local/my_secret
# Clean up
hasp delete file:///tmp/my-secret
Where to next
- New here? Start with Installation and the Quick Start.
- Want the mental model? Read How Hasp Thinks - it’s the conceptual map for everything else.
- Looking for backend details? See Supported Backends.
- Want tab completion? See Shell Completions.
- Looking up a flag? Jump to the CLI Reference.
- Something broken? Try Troubleshooting.
Installation
Hasp ships as a single static binary. No runtime libraries are required
for the pure-Rust backends (env://, file://, keyring://). Cloud
SDKs are compiled in when their Cargo features are enabled.
Pre-built binaries
The fastest path on any supported OS:
# Linux x86_64
curl -L https://github.com/rustpunk/hasp/releases/latest/download/hasp-linux-x64.tar.gz \
| tar xz
sudo mv hasp /usr/local/bin/
Replace the asset name for other targets (hasp-macos-arm64.tar.gz,
hasp-macos-x64.tar.gz, hasp-windows-x64.zip). The full list is on
the releases page.
On macOS, if Gatekeeper blocks the binary the first time, allow it under System Settings → Privacy & Security, or strip the quarantine attribute:
xattr -d com.apple.quarantine /usr/local/bin/hasp
On Windows, drop hasp.exe somewhere on %PATH% (or run it from a
folder you’ve added to PATH).
Verifying build provenance
Every release artifact is published with a SLSA v1.0
build attestation signed via GitHub OIDC and sigstore. The attestation
proves the artifact was produced from a specific commit of
github.com/rustpunk/hasp by the official release workflow — it
defends against the supply-chain class demonstrated by the Bitwarden CLI
npm compromise.
The simplest verification is with the GitHub CLI:
gh attestation verify hasp-linux-x64.tar.gz \
--owner rustpunk
This queries the GitHub attestations API, fetches the signed bundle, and verifies the artifact’s digest. No additional flags are needed — the workflow + commit + builder identity are all proven in one call.
For offline / air-gapped verification, the sigstore bundle is published
alongside each release artifact as <asset>.intoto.jsonl. Pass it to
any sigstore-compatible verifier — see GitHub’s
verifying attestations offline
guide for the current toolchain (gh attestation verify --bundle plus
--no-api, or cosign verify-blob-attestation).
The attestation establishes Build L2 in the SLSA terminology: the build identity is verifiable, but the build environment is not isolated (hosted GitHub runners). It does not defend against a maintainer account compromise pushing a malicious release tag — that class requires SLSA L3 (isolation-of-build), which is deferred.
From source
Requires a stable Rust toolchain (install via rustup).
git clone https://github.com/rustpunk/hasp.git
cd hasp
# Default build — env, file, keyring, op, vault, bw, aws-sm, aws-ssm,
# gcp-sm, azure-kv
cargo build --release --bin hasp
# Or install straight to ~/.cargo/bin
cargo install --path crates/hasp-cli
The release binary lands at ./target/release/hasp.
Feature flags
| Feature | Default | Notes |
|---|---|---|
env | ✅ | Environment variables (env://) |
file | ✅ | Local filesystem (file://) |
keyring | ✅ | OS keyring (keyring://) |
op | ✅ | 1Password (op://) — needs 1Password CLI |
vault | ✅ | HashiCorp Vault (vault://) — needs VAULT_ADDR |
bw | ✅ | Bitwarden (bw://) — needs bw CLI |
aws-sm | ✅ | AWS Secrets Manager (aws-sm://) |
aws-ssm | ✅ | AWS Systems Manager Parameter Store (aws-ssm://) |
gcp-sm | ✅ | GCP Secret Manager (gcp-sm://) |
azure-kv | ✅ | Azure Key Vault (azure-kv://) |
Build a minimal binary with only the backends you need:
# env + file only — tiny, no cloud deps
cargo build --release --bin hasp --no-default-features --features env,file
# Just the cloud providers you actually use
cargo build --release --bin hasp --no-default-features \
--features env,file,aws-sm,aws-ssm
Cutting unused backends shrinks the binary and trims the dependency graph; functionally there’s no difference for the backends you keep.
Hardened builds — memory-lock feature
The memory-lock feature instructs hasp-core to lock the physical
memory pages backing every fetched secret, preventing them from being
swapped to disk or included in crash dumps. Enable it at build time:
cargo build --release --bin hasp \
--features hasp-core/memory-lock
What it does
| Platform | Calls made |
|---|---|
| Linux | mlock(addr, len) + madvise(MADV_DONTDUMP) + madvise(MADV_WIPEONFORK) |
| macOS | mlock(addr, len) |
| Windows | VirtualLock(addr, len) |
MADV_DONTDUMP excludes the pages from /proc/<pid>/coredump_filter;
MADV_WIPEONFORK zeroes them in the child after fork(2).
Graceful degrade
All calls are best-effort. On Linux, RLIMIT_MEMLOCK defaults to
64 KiB for unprivileged users — once that budget is exhausted, mlock
returns EAGAIN and the secret remains usable but swappable. The
MitigationOutcome records whether each call succeeded; pass
--verbose to surface a summary in a future hasp release.
What it does NOT promise
- Secrets already in the kernel’s page cache (e.g., from
file://read) beforemlockwas called may still be swappable until the call lands. - Core dump exclusion does not prevent
/proc/<pid>/memreads by a same-uid process — that requiresPR_SET_DUMPABLE(already applied byhasp-core::hardeningregardless of this feature). - SLSA / supply-chain guarantees are independent of runtime memory posture.
Verify the install
hasp --version
# hasp 0.1.0-alpha
# Smoke test with no auth required
export HASP_SMOKE="ok"
hasp get env://HASP_SMOKE
If any of these fail, see Troubleshooting.
Next steps
- Quick Start — fetch, store, and delete a real secret.
- How Hasp Thinks — the mental model.
Quick Start
This walkthrough takes you from a fresh hasp install to managing
secrets across two backends in under five minutes. Everything here uses
only env:// and file:// — no cloud accounts, no tokens, no setup.
Step 1 — your first get
Create a secret using the environment, then read it back:
export DEMO_SECRET="hello-from-hasp"
hasp get env://DEMO_SECRET
Output:
hello-from-hasp
The value goes to stdout; that’s the contract. Everything else goes to stderr, so piping works cleanly.
Step 2 — check and delete
See whether a secret exists before you use it:
hasp exists env://DEMO_SECRET && echo "present" || echo "missing"
Delete it and confirm it’s gone:
hasp delete env://DEMO_SECRET
hasp exists env://DEMO_SECRET || echo "confirmed deleted"
delete exits 0 on success. exists exits 0 when the secret is
present and 1 when it is absent, making it natural for shell
conditionals.
Step 3 — write to a file
Put a value directly, or omit it to be prompted securely:
hasp put file:///tmp/hasp-demo "shh"
hasp get file:///tmp/hasp-demo
# shh
# Prompt mode (no echo in TTY)
hasp put file:///tmp/hasp-demo
# Value: ·····
The file backend creates parent directories automatically.
Step 4 — profile aliases
Typing env:// every time gets old. Define shortcuts in
~/.config/hasp/profiles.toml:
mkdir -p ~/.config/hasp
cat > ~/.config/hasp/profiles.toml <<'EOF'
[profiles.demo]
secret = "env://DEMO_SECRET"
token = "file:///tmp/hasp-demo"
[profiles.local]
db_password = "env://DB_PASSWORD"
EOF
Now use the alias:
export DEMO_SECRET="aliased-value"
hasp get "@demo/secret"
# aliased-value
If a profile contains a key with the same name as the profile itself
(e.g. [profiles.foo] with foo = "file:///etc/foo"), the bare alias
@foo resolves to that URL. Otherwise you must write @foo/key.
Step 5 — pipe-friendly output
The default output is plaintext to stdout. No JSON wrappers, no quotes, no trailing newline tricks:
hasp get "@demo/secret" | wc -c
# 13
# Use in a command substitution
MY_TOKEN=$(hasp get "@demo/token")
# MY_TOKEN is exactly the bytes stored, no extra whitespace
For file:// values that were written with a trailing newline, hasp
strips it automatically on read. This means:
echo -n "no-newline" | hasp put file:///tmp/clean -
hasp get file:///tmp/clean | xxd | tail -1
# no trailing 0a
Where to next
- How Hasp Thinks — the abstractions everything is built on.
- Profile Aliases — deeper alias resolution, env overrides, and config layout.
- Supported Backends — what operations work where, and what each URL looks like.
How Hasp Thinks
Understanding three core ideas makes everything else in hasp click:
- Every secret is a URL.
- Ambient credentials only.
- The CLI is a thin shell over the library.
Every secret is a URL
Hasp identifies every secret with a URL. The scheme selects the backend; the rest is backend-specific addressing. This is the primary reason one tool can speak to many stores without a sprawl of subcommands.
env://MY_API_KEY
file:///etc/secrets/db-password
keyring://my-service/admin
aws-sm://us-east-1/prod/db-password
vault://127.0.0.1/secret/data/prod/db-password
op://vault/item/field
bw://item/field
gcp-sm://my-project/my-secret
azure-kv://my-vault/my-secret
The URL is the identifier. You pass it to get, put, list,
delete, or exists. The backend handles the rest.
Why URLs?
- They are self-describing. You can tell it’s AWS by the scheme.
- They have a standard parser. No custom grammar to learn.
- They work in env vars and config files. Drop a URL into any
.envor TOML file and it’s readable without further annotation. - They compose with aliases.
@prod/db_passwordexpands to a full URL at runtime, so the human reads an alias and the machine gets a URL.
Ambient credentials only
Hasp does not store credentials, rotate tokens, bootstrap auth, or manage sessions. It reads what your environment already provides.
| Backend | What hasp reads |
|---|---|
env:// | The env var itself |
file:// | The file contents |
keyring:// | The OS keyring (keychain, secret-service, etc.) |
op:// | The 1Password CLI (op read) |
vault:// | VAULT_ADDR + VAULT_TOKEN (or ~/.vault-token) |
bw:// | BW_SESSION + the bw CLI |
aws-sm:// | AWS SDK default chain (env, profile, IAM role) |
aws-ssm:// | AWS SDK default chain |
gcp-sm:// | gcloud auth or ADC |
azure-kv:// | Azure SDK default chain |
This is a deliberate boundary. Hasp is a secret consumer, not a secret lifecycle manager. If you need to set up a Vault token, configure AWS SSO, or unlock Bitwarden, do that before invoking hasp.
What this means in practice
- On a developer laptop: 1Password is unlocked, Vault token is in
~/.vault-token, AWS SSO session is current. Hasp just reads them. - In CI: The runner has
AWS_ROLE_ARN,VAULT_TOKEN, orBW_SESSIONinjected as env vars. Hasp just reads them. - On a server: The EC2 instance has an IAM role. Hasp uses the IMDS endpoint transparently.
The CLI is a thin shell over the library
hasp-cli is a thin wrapper. It parses arguments, resolves aliases,
reads stdin when needed, and calls hasp::Store methods. All the real
logic lives in hasp (the router) and hasp-core + backend crates.
This matters because:
- Library consumers get the same behavior. A Rust program using
haspas a crate gets identical URL parsing, alias resolution, and backend dispatch. - Errors are typed in the library. The CLI translates them to
human strings, but the underlying types are rich enough for code to
match on
hasp::Error::NotFound,PermissionDenied,AuthenticationFailed, etc. - No CLI-only behavior. You won’t find a feature that works on the command line but is inaccessible from Rust code.
The five operations
Hasp exposes exactly five operations. Every backend implements the subset it supports.
| Operation | Meaning | Example |
|---|---|---|
get | Read a secret value | store.get("env://HOME") |
put | Write a secret value | store.put("file:///tmp/secret", &secret) |
list | List entries under a prefix or collection | store.list("vault://127.0.0.1/secret/") |
delete | Remove a secret | store.delete("file:///tmp/secret") |
exists | Check presence (exit 0 = present, 1 = absent) | store.exists("env://HOME") |
Library entry points
The library provides three ways to obtain a Store:
use hasp::Store;
// All enabled backends (depends on Cargo features)
let store = Store::with_defaults();
// Custom subset, e.g. only env and file
let store = Store::with_backends(vec![
hasp::env(),
hasp::file(),
]);
// Empty store, then register backends manually
let mut store = Store::empty();
store.register(hasp::env());
For one-off usage, free functions construct a default store internally:
let secret = hasp::get("env://HOME")?;
hasp::put("file:///tmp/secret", &secret)?;
hasp::exists("env://HOME")?;
These match the sibling-crate ferrule convention and are thin wrappers
over Store::with_defaults(). Heavy callers should cache a Store
instance to avoid repeated backend construction.
hasp also exposes bulk operations:
let values = store.batch_get(
&["env://A", "env://B", "env://A"]
)?;
// values.len() == 2 — deduplicated and resolved concurrently.
store.bulk_put(&[
("file:///tmp/a", &val_a),
("file:///tmp/b", &val_b),
])?;
Not every backend supports every operation. For example, env://
does not support put (environment variables are read-only after
process start) and does not support list. Hasp returns a clear
error when you try an unsupported operation.
Error taxonomy
When something goes wrong, hasp tells you what category of problem it is. This helps you decide whether to retry, re-authenticate, or fix a URL.
| Kind | When it happens | What to check |
|---|---|---|
UrlParse | The string isn’t a valid URL | Scheme, slashes, encoding |
InvalidUrl | The URL doesn’t match backend grammar | Path segments, query params |
UnknownScheme | The scheme has no registered backend | Feature flag, backend name |
UnsupportedOperation | Backend doesn’t do this | The backends table |
NotFound | Secret doesn’t exist | Name, permission, region |
PermissionDenied | Credentials valid, but not authorized | IAM / RBAC / policies |
AuthenticationFailed | Credentials missing or expired | Token, session, CLI login |
PreconditionFailed | Resource in wrong state | Soft-deleted, wrong version |
Backend::{Transient,Throttled,Permanent} | Upstream error | Retry, wait, or escalate |
See Troubleshooting for the most common scenarios.
Retry decorator
For HTTP-backed stores (aws-sm, aws-ssm, vault, gcp-sm,
azure-kv) you can opt into automatic retry on transient failures:
use hasp::StoreBuilder;
use std::time::Duration;
let store = StoreBuilder::with_defaults()
.with_retry(3, Duration::from_millis(100))
.build();
The decorator wraps each HTTP backend with exponential backoff plus
jitter. Local backends (env, file, keyring, op, bw) are
never wrapped — their errors are not transient.
Diagnostics
Store::resolve inspects a URL without performing I/O, returning the
scheme, the chosen backend name, and whether the entry is still in
the TTL cache. This powers the CLI --explain flag:
$ hasp --explain get env://HOME
URL: env://HOME
Backend: env
Cache: miss
Use it to debug alias expansion, proxy routing, or cache state before running a destructive command.
Cross-backend migration with cp
Because URLs are uniform across stores, copying a secret from one backend to another is a single command:
# Move a value from a local file into Vault
hasp cp file:///etc/secrets/db.txt vault://kv/data/myapp/db
# Roll a credential between environments after rotation
hasp cp @prod/db @stage/db --yes --verify
hasp cp is the only verb that reads and writes a secret in one
invocation, so it ships with stricter defaults than the other verbs
(refuses to overwrite by default, refuses cross-environment copies
unless --yes, refuses plain-http proxies unless
HASP_ALLOW_HTTP_PROXY=1). Add environment = "..." to a profile
entry in profiles.toml to enable the cross-environment safety net:
[profiles.prod]
environment = "prod"
db = "aws-sm://us-east-1/prod/db-password"
[profiles.stage]
environment = "stage"
db = "aws-sm://us-east-1/stage/db-password"
See CLI Reference: hasp cp for
the full flag set and security model.
Drift detection with diff
hasp diff is the read-only sibling of cp: it answers the
“did staging and prod drift?” question without printing either value.
hasp diff @stage/db @prod/db # exits 0 on match, 1 on differ
hasp diff aws-sm://us-east-1/api vault://kv/data/api?field=token
The verdict is binary by construction — match or differ. A mismatch
reveals no byte counts, common prefixes, or hashes; the compare path
uses constant-time equality so timing channels are not informative
either. Cross-environment refusal and plain-http proxy refusal mirror
cp. See CLI Reference: hasp diff
for the full flag set.
Where to next
- Profile Aliases — alias resolution rules, config file format, and environment overrides.
- Supported Backends — URL grammar, supported operations, and auth per backend.
Profile Aliases
Aliases let you type @prod/db_password instead of a 60-character
AWS ARN or a nested Vault path. They live in profiles.toml, are
resolved at runtime, and are entirely optional.
Config file location
Hasp looks for profiles.toml in this order:
- The path in the
HASP_PROFILES_PATHenvironment variable. - The platform config directory:
- Linux:
~/.config/hasp/profiles.toml - macOS:
~/Library/Application Support/hasp/profiles.toml - Windows:
%APPDATA%/hasp/profiles.toml
- Linux:
If the file does not exist, aliases simply don’t resolve (you’ll get
unknown profile alias when you try to use one).
File format
Profiles are flat TOML tables under [profiles.<name>]:
[profiles.prod]
db_password = "aws-sm://us-east-1/prod/db-password"
api_key = "op://Production/API/credential"
[profiles.staging]
db_password = "aws-sm://us-east-1/staging/db-password"
[profiles.local]
db_password = "env://DB_PASSWORD"
Each key is a string value that must be a valid hasp URL.
Alias resolution rules
The text after @ is parsed as profile[/key].
| Alias | Resolves to |
|---|---|
@prod/db_password | aws-sm://us-east-1/prod/db-password |
@prod/api_key | op://Production/API/credential |
@local/db_password | env://DB_PASSWORD |
Self-key shorthand
If a profile contains a key with the same name as the profile
itself, the bare alias @name resolves to that key:
[profiles.prod]
prod = "env://PROD_TOKEN"
db_password = "aws-sm://us-east-1/prod/db-password"
| Alias | Resolves to |
|---|---|
@prod | env://PROD_TOKEN |
@prod/db_password | aws-sm://us-east-1/prod/db-password |
Without a self-key, the bare alias is an error:
[profiles.local]
db_password = "env://DB_PASSWORD"
| Alias | Result |
|---|---|
@local | Error: unknown profile alias: @local |
@local/db_password | env://DB_PASSWORD |
This prevents ambiguity: a bare @name only works when you’ve
explicitly defined it as a shorthand.
Trust model — hasp profile allow
A profiles.toml that was silently modified (by a compromised
installer, a synced dotfiles repo, or an over-broad chmod) can
redirect every hasp invocation to attacker-controlled URLs without
any visible warning. The profile allow model closes this gap.
How it works
-
After writing or updating
profiles.toml, run:hasp profile allowThis records the file’s mtime and SHA-256 fingerprint in
profiles.allowed(same directory,0600on Unix). -
To enforce the check for every subsequent invocation, set:
export HASP_REQUIRE_PROFILE_ALLOW=1With this variable set,
hasprefuses any command that would use profile aliases unless the currentprofiles.tomlexactly matches the recorded fingerprint. Any modification invalidates trust — runallowagain after reviewing the change. -
Check the current status:
hasp profile showOutput:
Path: /home/user/.config/hasp/profiles.toml Mtime: 2026-05-14T12:34:56Z Allowed: yes (last allowed at 2026-05-14T12:34:56Z)
CI environments
In a CI pipeline where profiles.toml is checked into source control
and cannot be re-allowed interactively, use:
# Either: bypass per-invocation
hasp --no-profile-allow get @ci/secret
# Or: do not set HASP_REQUIRE_PROFILE_ALLOW in CI at all
# (enforcement is opt-in; pipelines that never set the var are unaffected)
Opt-in status
Enforcement is opt-in for the current release cycle. The intent is to
make it the default in a subsequent release after observing user
workflows. Profiles without any allow record are silently trusted when
HASP_REQUIRE_PROFILE_ALLOW is unset.
No sensitive data in profiles.toml
Profile files contain only names and URLs. Never put secret values in the file — that’s what the backends are for. The file is safe to check into a dotfiles repo or share across a team.
Runtime override
You can point hasp at a different profile file per invocation:
HASP_PROFILES_PATH=/tmp/ci-profiles.toml hasp get @ci/secret
This is useful for CI pipelines that generate profiles dynamically or for testing without touching your main config.
Next steps
- Supported Backends — what each backend URL looks like.
- CLI Reference — flags and subcommands.
Supported Backends
Hasp supports ten secret stores. The default binary includes all of them; you can trim the set at compile time via Cargo features.
Operations matrix
| Backend | get | put | list | delete | exists |
|---|---|---|---|---|---|
env:// | ✅ | ❌ | ❌ | ❌ | ✅ |
file:// | ✅ | ✅ | ✅ | ✅ | ✅ |
keyring:// | ✅ | ✅ | ❌ | ✅ | ✅ |
op:// | ✅ | ❌ | ❌ | ❌ | ✅ |
vault:// | ✅ | ❌ | ❌ | ❌ | ✅ |
bw:// | ✅ | ❌ | ❌ | ❌ | ✅ |
aws-sm:// | ✅ | ❌ | ❌ | ❌ | ✅ |
aws-ssm:// | ✅ | ❌ | ❌ | ❌ | ✅ |
gcp-sm:// | ✅ | ❌ | ❌ | ❌ | ✅ |
azure-kv:// | ✅ | ❌ | ❌ | ❌ | ✅ |
env:// — Environment variables
hasp get env://HOME
hasp exists env://NONEXISTENT || echo "missing"
- Read-only.
putanddeleteare unsupported; set or unset the variable through your shell. - No list. Environment variables are not enumerable via hasp.
file:// — Local filesystem
hasp get file:///etc/secrets/db-password
hasp put file:///tmp/secret "my-value"
hasp delete file:///tmp/secret
# List with glob (shell-quote the pattern to prevent shell expansion)
hasp list 'file:///etc/secrets/*.key'
hasp list 'file:///etc/secrets/**/*.key'
-
Creates parent directories on
put. -
Trims trailing newlines on
get. A value written as"secret\n"is read back as"secret". -
Permissions are whatever your umask produces; hasp does not force
0600. -
Glob
list: the path component may contain Unix shell glob patterns (*,**,?,[abc]).**traverses subdirectories. Shell-quote the URL to prevent early expansion.Query param Default Meaning ?hidden=1off Include dotfiles ?follow_symlinks=1off Follow symlinks during **traversalSymlinks are excluded by default to prevent glob patterns from escaping the intended directory tree. Only regular files are returned (no directories). Each returned entry URL is directly
get-able.
keyring:// — OS keyring
hasp get keyring://my-service/admin
hasp put keyring://my-service/admin "new-password"
hasp delete keyring://my-service/admin
- Target on Linux:
keyring://service/account - Uses the platform keyring (secret-service on Linux, Keychain on macOS, Credential Manager on Windows).
op:// — 1Password
hasp get op://Production/API/credential
hasp exists op://Production/API/credential
- Requires the 1Password CLI (
op) to be installed and signed in. - Read-only via hasp.
vault:// — HashiCorp Vault
hasp get vault://127.0.0.1/secret/data/prod/db-password
- Requires
VAULT_ADDRand a Vault token (env var or~/.vault-token). - Read-only via hasp.
bw:// — Bitwarden
hasp get bw://item/field
- Requires
bwCLI installed andBW_SESSIONset. - Read-only via hasp.
aws-sm:// — AWS Secrets Manager
hasp get aws-sm://us-east-1/prod/db-password
- Uses the AWS SDK default credential chain.
- Read-only via hasp.
aws-ssm:// — AWS Systems Manager Parameter Store
hasp get aws-ssm://us-east-1/prod/db-password
- Uses the AWS SDK default credential chain.
- Read-only via hasp.
gcp-sm:// — GCP Secret Manager
hasp get gcp-sm://my-project/my-secret
- Uses Application Default Credentials (ADC) or
gcloudauth. - Read-only via hasp.
azure-kv:// — Azure Key Vault
hasp get azure-kv://my-vault/my-secret
- Uses the Azure SDK default credential chain.
- Read-only via hasp.
What “read-only via hasp” means
Several backends do not support put or delete through hasp. This
is usually because the upstream CLI or API has additional constraints
(encryption at rest settings, versioning, soft-delete, etc.) that make
a generic “just write it” unsafe or impossible.
If you need write/delete on these backends, use their native CLIs or APIs directly.
HTTP CONNECT Proxy
Corporate networks often force outbound traffic through an HTTP CONNECT
proxy (Squid, Blue Coat, Zscaler, etc.). hasp supports explicit proxy
configuration for HTTP-based backends (vault://, gcp-sm://,
azure-kv://). AWS SDK backends (aws-sm://, aws-ssm://) honour
HTTPS_PROXY / HTTP_PROXY environment variables.
Quick start
hasp get --proxy-url http://proxy.corp.example.com:8080 \
vault://secret/data/myapp/db-password
This routes every HTTP request through proxy.corp.example.com:8080.
Proxy configuration layers
Three layers, first hit wins:
--proxy-url <URL>CLI flag.proxy_url = "..."in the active profile (profiles.toml).ALL_PROXY,HTTPS_PROXY, orHTTP_PROXYenv vars.
NO_PROXY is honoured at layer 3. Layers 1 and 2 are explicit user
intent and therefore bypass NO_PROXY.
Example profile
[profiles.corp]
proxy_url = "http://proxy.corp.example.com:8080"
db_password = "vault://secret/data/myapp/db-password"
When you run hasp get @corp/db_password, the proxy from the corp
profile is used automatically.
Authenticated proxies
Include credentials in the URL:
hasp get --proxy-url http://user:pass@proxy.corp.example.com:8080 \
vault://secret/data/myapp/db-password
The credentials are sent via Proxy-Authorization: Basic <base64> during
the CONNECT handshake. Internally, the password is wrapped in
secrecy::SecretString so it never appears in Debug output or error
messages.
Backend support matrix
| Backend | Explicit proxy (--proxy-url, profile) | Env vars (HTTP_PROXY, NO_PROXY) |
|---|---|---|
vault:// | ✅ via reqwest::Proxy (HTTP CONNECT / SOCKS5) | ✅ reqwest default |
gcp-sm:// | ✅ via reqwest::Proxy (HTTP CONNECT / SOCKS5) | ✅ reqwest default |
azure-kv:// | ✅ via reqwest::Proxy (HTTP CONNECT / SOCKS5) | ✅ reqwest default |
aws-sm:// | ⚠️ not yet; use env vars | ✅ AWS SDK default chain |
aws-ssm:// | ⚠️ not yet; use env vars | ✅ AWS SDK default chain |
op:// | N/A (delegates to op CLI) | N/A |
bw:// | N/A (delegates to bw CLI) | N/A |
keyring:// | N/A (OS IPC) | N/A |
file:// / env:// | N/A (no network) | N/A |
NO_PROXY syntax
NO_PROXY supports the same rules as curl:
*— disables proxy for every host.localhost,127.0.0.1— exact matches, comma-separated..example.com— suffix match (db.example.comhits,example.comdoes not).- Port numbers in patterns are ignored.
Example:
export HTTP_PROXY=http://proxy.corp.example.com:8080
export NO_PROXY="localhost,127.0.0.1,.internal.example.com"
hasp get vault://vault.internal.example.com/secret/data/db
# → NOT proxied (matches .internal.example.com)
hasp get vault://vault.external.example.com/secret/data/db
# → proxied through proxy.corp.example.com:8080
SOCKS5
SOCKS5 proxies are supported for the same HTTP-based backends as HTTP
CONNECT (vault://, gcp-sm://, azure-kv://). Pass the URL with a
socks5:// scheme:
hasp get --proxy-url socks5://127.0.0.1:1080 \
vault://secret/data/myapp/db-password
Only unauthenticated SOCKS5 is supported at this time. If your proxy requires username/password authentication, use a local forwarder or file a feature request.
Shell Completions
Hasp supports two completion modes: a static AOT subcommand for
packaging and a dynamic runtime system powered by
clap_complete that completes profile aliases, URL schemes, and
file paths at tab-press time.
Static completion scripts
Generate a completion script once and install it in your shell’s completion directory:
Bash
hasp complete bash > /usr/share/bash-completion/completions/hasp
# or user-local:
hasp complete bash > ~/.local/share/bash-completion/completions/hasp
Zsh
hasp complete zsh > "${fpath[1]}/_hasp"
Fish
hasp complete fish > ~/.config/fish/completions/hasp.fish
PowerShell
hasp complete powershell | Out-String | Invoke-Expression
# Or save to your profile
The complete subcommand is hidden from --help because it is a
packaging concern, not a daily user workflow.
Dynamic completions (recommended)
Dynamic completions use clap_complete::CompleteEnv to call back into
hasp on every tab press. This lets hasp suggest:
- URL schemes —
hasp get en<TAB>→env:// - Environment variable names —
hasp get env://<TAB>→ available env vars - Profile aliases —
hasp get @prod/<TAB>→ keys in the prod profile fromprofiles.toml - File paths —
hasp get file:///tmp/<TAB>→ native filesystem completion
To enable them, source the registration snippet for your shell:
Bash
echo 'source <(COMPLETE=bash hasp)' >> ~/.bashrc
Zsh
echo 'source <(COMPLETE=zsh hasp)' >> ~/.zshrc
Fish
echo 'COMPLETE=fish hasp | source' >> ~/.config/fish/completions/hasp.fish
PowerShell
echo '$env:COMPLETE = "powershell"; hasp | Out-String | Invoke-Expression; Remove-Item Env:\COMPLETE' >> $PROFILE
How it works
The registration script emitted by COMPLETE=<shell> hasp is a thin
wrapper that calls back into hasp with the current command-line words.
Hasp parses the partial command, identifies which argument is being
completed, and returns candidates:
$ hasp get en<TAB>
env://
$ hasp get @prod/<TAB>
@prod/db_password
@prod/api_key
Because the completion logic lives in the same binary as the CLI, it is always in sync — no mismatched completion scripts after upgrades.
Disabling dynamic completions
Set COMPLETE= or COMPLETE=0:
COMPLETE= hasp get env://HOME
Completion internals
The complete subcommand uses
clap_complete
to generate shell-specific completion scripts. The dynamic path uses
clap_complete::engine::ArgValueCompleter attached to every address
argument in the derive macro, so custom completion logic is co-located
with the argument definition.
Next steps
- CLI Reference — all flags and subcommands.
- Troubleshooting — if completions aren’t appearing.
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.
Troubleshooting
Common errors
unknown profile alias: @foo
- The profile
foois not defined inprofiles.toml, or the keyfoodoesn’t exist in the profile. - Check:
cat ~/.config/hasp/profiles.toml - For bare
@foo, the profile must contain a self-key:[profiles.foo] foo = "env://FOO"
unsupported scheme: xyz
- The scheme
xyzhas no registered backend. - Check the scheme spelling. Valid schemes:
env,file,keyring,op,vault,bw,aws-sm,aws-ssm,gcp-sm,azure-kv. - If the scheme is correct, the backend may not be compiled in.
Rebuild with the appropriate feature flag:
cargo build --release --bin hasp --features aws-sm
not found: ...
- The secret does not exist at the given URL.
- Verify the URL path/region/secret name.
- For cloud backends, verify the credentials have access.
authentication failed: ...
- Missing or expired ambient credentials.
- Check the per-backend requirements:
vault://—VAULT_ADDRandVAULT_TOKEN(or~/.vault-token)op://—opCLI signed inbw://—BW_SESSIONsetaws-sm:///aws-ssm://—AWS_ACCESS_KEY_IDor IAM rolegcp-sm://—gcloud author ADCazure-kv://— Azure CLI login or managed identity
permission denied: ...
- Credentials are valid, but not authorized for this resource.
- Check IAM / RBAC / key vault access policies / GCP IAM bindings.
env does not support put
env://is read-only. Set the variable through your shell before invoking hasp.
hasp init says the file already exists
hasp initrefuses to overwrite an existingprofiles.tomlto prevent accidental data loss.- Use
hasp init --forceto overwrite, or delete the file first.
does not support list
env://andfile://do not supportlist. Uselistwithvault://,aws-sm://, or other backends that expose collections.
Completions not appearing
Static completions
- Ensure the script was written to the correct directory:
- Bash:
~/.local/share/bash-completion/completions/haspor/usr/share/bash-completion/completions/hasp - Zsh: somewhere in
$fpath - Fish:
~/.config/fish/completions/hasp.fish
- Bash:
- Open a new shell or run the source command for your shell
(
source ~/.bashrc,source ~/.zshrc, etc.).
Dynamic completions
- Ensure the registration snippet is sourced in your shell config:
source <(COMPLETE=bash hasp) - Check that
haspis on$PATHwhen the shell starts. - Set
COMPLETE=to disable and test raw:COMPLETE= bash # Now try completion
backend 'vault' failed: proxy error: ...
- Vault (and other HTTP backends) can route through an HTTP CONNECT proxy.
- Quick fix: set standard environment variables:
export HTTPS_PROXY=http://proxy.corp.example.com:8080 export NO_PROXY="localhost,127.0.0.1" - Or use the explicit
--proxy-urlflag:hasp get --proxy-url http://proxy:8080 vault://secret/data/db - For authenticated proxies, see HTTP CONNECT Proxy.
- AWS backends (
aws-sm://,aws-ssm://) do not yet support explicit proxy configuration. UseHTTPS_PROXYenv vars for those.
Next steps
- Installation — verify your build.
- Supported Backends — per-backend auth details.
- Profile Aliases — config file format.
- Shell Completions — setup instructions.