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

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.