Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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.toml and type @prod/db_password instead 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

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

FeatureDefaultNotes
envEnvironment variables (env://)
fileLocal filesystem (file://)
keyringOS keyring (keyring://)
op1Password (op://) — needs 1Password CLI
vaultHashiCorp Vault (vault://) — needs VAULT_ADDR
bwBitwarden (bw://) — needs bw CLI
aws-smAWS Secrets Manager (aws-sm://)
aws-ssmAWS Systems Manager Parameter Store (aws-ssm://)
gcp-smGCP Secret Manager (gcp-sm://)
azure-kvAzure 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

PlatformCalls made
Linuxmlock(addr, len) + madvise(MADV_DONTDUMP) + madvise(MADV_WIPEONFORK)
macOSmlock(addr, len)
WindowsVirtualLock(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) before mlock was called may still be swappable until the call lands.
  • Core dump exclusion does not prevent /proc/<pid>/mem reads by a same-uid process — that requires PR_SET_DUMPABLE (already applied by hasp-core::hardening regardless 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

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

Understanding three core ideas makes everything else in hasp click:

  1. Every secret is a URL.
  2. Ambient credentials only.
  3. 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 .env or TOML file and it’s readable without further annotation.
  • They compose with aliases. @prod/db_password expands 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.

BackendWhat 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, or BW_SESSION injected 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 hasp as 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.

OperationMeaningExample
getRead a secret valuestore.get("env://HOME")
putWrite a secret valuestore.put("file:///tmp/secret", &secret)
listList entries under a prefix or collectionstore.list("vault://127.0.0.1/secret/")
deleteRemove a secretstore.delete("file:///tmp/secret")
existsCheck 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.

KindWhen it happensWhat to check
UrlParseThe string isn’t a valid URLScheme, slashes, encoding
InvalidUrlThe URL doesn’t match backend grammarPath segments, query params
UnknownSchemeThe scheme has no registered backendFeature flag, backend name
UnsupportedOperationBackend doesn’t do thisThe backends table
NotFoundSecret doesn’t existName, permission, region
PermissionDeniedCredentials valid, but not authorizedIAM / RBAC / policies
AuthenticationFailedCredentials missing or expiredToken, session, CLI login
PreconditionFailedResource in wrong stateSoft-deleted, wrong version
Backend::{Transient,Throttled,Permanent}Upstream errorRetry, 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:

  1. The path in the HASP_PROFILES_PATH environment variable.
  2. The platform config directory:
    • Linux: ~/.config/hasp/profiles.toml
    • macOS: ~/Library/Application Support/hasp/profiles.toml
    • Windows: %APPDATA%/hasp/profiles.toml

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].

AliasResolves to
@prod/db_passwordaws-sm://us-east-1/prod/db-password
@prod/api_keyop://Production/API/credential
@local/db_passwordenv://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"
AliasResolves to
@prodenv://PROD_TOKEN
@prod/db_passwordaws-sm://us-east-1/prod/db-password

Without a self-key, the bare alias is an error:

[profiles.local]
db_password = "env://DB_PASSWORD"
AliasResult
@localError: unknown profile alias: @local
@local/db_passwordenv://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

  1. After writing or updating profiles.toml, run:

    hasp profile allow
    

    This records the file’s mtime and SHA-256 fingerprint in profiles.allowed (same directory, 0600 on Unix).

  2. To enforce the check for every subsequent invocation, set:

    export HASP_REQUIRE_PROFILE_ALLOW=1
    

    With this variable set, hasp refuses any command that would use profile aliases unless the current profiles.toml exactly matches the recorded fingerprint. Any modification invalidates trust — run allow again after reviewing the change.

  3. Check the current status:

    hasp profile show
    

    Output:

    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

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

Backendgetputlistdeleteexists
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. put and delete are 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 paramDefaultMeaning
    ?hidden=1offInclude dotfiles
    ?follow_symlinks=1offFollow symlinks during ** traversal

    Symlinks 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_ADDR and a Vault token (env var or ~/.vault-token).
  • Read-only via hasp.

bw:// — Bitwarden

hasp get bw://item/field
  • Requires bw CLI installed and BW_SESSION set.
  • 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 gcloud auth.
  • 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:

  1. --proxy-url <URL> CLI flag.
  2. proxy_url = "..." in the active profile (profiles.toml).
  3. ALL_PROXY, HTTPS_PROXY, or HTTP_PROXY env 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

BackendExplicit 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.com hits, example.com does 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 use clap_complete::CompleteEnv to call back into hasp on every tab press. This lets hasp suggest:

  • URL schemeshasp get en<TAB>env://
  • Environment variable nameshasp get env://<TAB> → available env vars
  • Profile aliaseshasp get @prod/<TAB> → keys in the prod profile from profiles.toml
  • File pathshasp 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

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.

CodeMeaning
0Success.
1Usage or local error (bad flags, malformed URL, unknown scheme, unsupported operation, IO error).
2Not found (the URL is well-formed; the secret does not exist).
3Permission denied (the caller’s credentials are valid but lack access).
4Transport / network failure (transient or throttled). Retry may help.
5Authentication failed (credentials missing, invalid, or expired).
6Precondition failed (e.g. cp cross-environment refusal, plain-http proxy refusal, --verify mismatch, --if-exists=fail blocked).
7Backend 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.
  • -F is 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 with not found: field '...' not found (exit code 2). Non-scalar leaves (objects, arrays, null) fail with invalid 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 via rpassword.

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 separated
      • table — aligned columns, human-readable
      • json — 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 support get.
    • dst — Destination URL or alias. Backend must support put.
  • Flags:
    • --if-exists <fail|overwrite|skip> — Disposition when dst already holds a value. Default fail.
    • -f, --force — Shorthand for --if-exists=overwrite.
    • --verify — Re-read dst after 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 for cp: resolves both URLs and prints the plan; does not call get or put.
  • Exit codes: Refusals (cross-environment, plain-http proxy, self-copy, --verify mismatch, --if-exists=fail blocked) return code 6 (precondition). See Exit codes.

Security model

The defaults are deliberately stricter than Unix cp:

  1. --if-exists=fail is 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.
  2. Self-copy refused. hasp cp file:///x file:///x returns an error. Prevents version-counter inflation on backends that version writes (AWS Secrets Manager, Azure Key Vault, GCP Secret Manager).
  3. Cross-environment refusal. When both src and dst are profile aliases AND both profiles declare an environment = "..." key in profiles.toml, a mismatch refuses without --yes. Absent labels disable the check (backwards-compatible).
  4. Plain-http proxy refusal. When HTTP_PROXY / HTTPS_PROXY / --proxy-url resolves to an http:// URL, cp refuses unless HASP_ALLOW_HTTP_PROXY=1 is set. The doubled-exposure window of cp makes MITM more costly than for other verbs.
  5. Audit events. cp.start and cp.done are emitted by the library; see Audit events for the wire format and how to redirect or disable them.
  6. --verify uses constant-time comparison via subtle::ConstantTimeEq. A failed verify returns a generic “verify failed: source and destination differ” message with no byte-level diff.
  7. No atomicity across backends. A failed put(dst) after a successful get(src) leaves dst in an indeterminate state — either untouched or partially written, depending on the dst backend’s semantics. hasp cannot promise two-phase commit across heterogeneous stores.
  8. cp copies the value, not the access policy. Copying from a tightly-controlled store to a loosely-controlled one effectively widens access to the value. hasp has 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 support get.
  • Flags:
    • -y, --yes — Confirm a cross-environment comparison (mirrors cp).
    • --explain (global) — Resolves both URLs and prints the plan; does not call get.
  • Exit codes:
    • 0 — Both secrets compared byte-equal.
    • 1 — Secrets differ (length or content), OR a usage error (parallels hasp 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 crate cp --verify uses) so timing channels are not informative about a near-match. Both secret bodies stay inside SecretString end-to-end and are dropped before diff returns.
  • Audit events: diff.start / diff.done with outcome "match" / "differ" / "error". See Audit events.
  • Cross-environment refusal: same shape as cp. Aliases whose profiles declare mismatched environment = "..." 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=URL is repeatable. Each URL is resolved through the configured Store; -e DB_PASS=@prod/db also 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 like hasp run -- echo $DB_PASS would expose secrets in the terminal scroll buffer regardless of which stream the child writes to. Pass --allow-tty to override for interactive debugging.
  • /proc/<pid>/environ visibility: 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 run feature — intercepts the child’s stdout to mask accidentally echoed secrets) is not yet implemented; scheduled as a follow-up.
  • Audit events: run.start and run.done are emitted; each intermediate get emits its own get.start/get.done events.

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_PATH if 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

FlagDescription
-h, --helpPrint help. Pass -h for a summary, --help for full help.
-q, --quietSuppress non-error informational output.
-v, --verboseIncrease output verbosity; prints operation traces to stderr. Can be used multiple times (-vv).
--no-cacheDisable the per-invocation in-process secret cache for this invocation.
--no-profile-allowSkip 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:

  1. --no-cache is passed on the command line.
  2. HASP_NO_CACHE=1 is set in the environment (also accepts true, yes, on).
  3. The CI environment 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>/mem inspection by a same-uid attacker (PR_SET_DUMPABLE=0 mitigates 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):

  1. HASP_AUDIT env var.
  2. ~/.config/hasp/audit.toml (override location with HASP_AUDIT_CONFIG_PATH).
  3. Default: stderr.

Env vars:

Env varValueBehaviour
HASP_AUDIT(unset)Use audit.toml if present, else stderr
HASP_AUDIToffSuppress all audit output
HASP_AUDITstderrWrite JSON lines to stderr
HASP_AUDITfileWrite to HASP_AUDIT_PATH (falls back to stderr if path is empty, NoopSink if open fails)
HASP_AUDITsyslogForward 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.logLog 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.tomlOverride 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 hasp can redirect or suppress the audit stream — for example by setting HASP_AUDIT=off before invoking hasp, 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 run hasp in a privilege-separated environment.
  • Concurrent writers. FileSink serializes writes via an internal Mutex<File>; concurrent invocations of hasp writing 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 by ts on ingest.
  • No value leakage. AuditEvent’s field set is closed (#[non_exhaustive]) and every field is either a timestamp, a &'static str from a closed set, or a URL scheme. No implementation of AuditSink can leak a value or a value-derived length.

Environment variables

VariableEffect
HASP_PROFILES_PATHOverride the default profiles.toml path.
HASP_ALLOW_HTTP_PROXYSet to 1 to allow hasp cp through a plain-http proxy.
HASP_AUDITAudit sink mode: unset=stderr (or audit.toml), off=silent, file=log file, syslog=local syslog daemon (Unix).
HASP_AUDIT_PATHPath for HASP_AUDIT=file mode (append, 0600 on Unix).
HASP_AUDIT_IDENTProgram ident for HASP_AUDIT=syslog mode (default "hasp").
HASP_AUDIT_CONFIG_PATHOverride ~/.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/key or @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 foo is not defined in profiles.toml, or the key foo doesn’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 xyz has 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_ADDR and VAULT_TOKEN (or ~/.vault-token)
    • op://op CLI signed in
    • bw://BW_SESSION set
    • aws-sm:// / aws-ssm://AWS_ACCESS_KEY_ID or IAM role
    • gcp-sm://gcloud auth or ADC
    • azure-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 init refuses to overwrite an existing profiles.toml to prevent accidental data loss.
  • Use hasp init --force to overwrite, or delete the file first.

does not support list

  • env:// and file:// do not support list. Use list with vault://, 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/hasp or /usr/share/bash-completion/completions/hasp
    • Zsh: somewhere in $fpath
    • Fish: ~/.config/fish/completions/hasp.fish
  • 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 hasp is on $PATH when 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-url flag:
    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. Use HTTPS_PROXY env vars for those.

Next steps