@hyperdrive.bot/airlock-cli
v0.1.25
Published
Airlock CLI — multi-tenant secrets vault with a just-in-time access broker for AI agents
Readme
@hyperdrive.bot/airlock-cli
airlock — the CLI for Airlock, a multi-tenant secrets vault with a
just-in-time access broker for AI agents.
Part of the
@hyperdrive.botproduct family. Backend + CLI design:docs/architecture/AIRLOCK-BACKEND-CLI-ARCHITECTURE.md.
Two planes
Airlock has two user planes, and the CLI auto-detects which one you're on by the
token class (not by a flag or by CI):
| Plane | Trigger | Auth | Plaintext access |
|---|---|---|---|
| Human | default | Cognito OAuth PKCE → SigV4 (auth login) | direct reveal (policy-gated, audited) |
| Agent | AIRLOCK_TOKEN starts with alk_agt_ | Bearer token | only via an approved, single-use grant |
A human running in CI with a Cognito session is still on the human plane; an
agent on a laptop with an alk_agt_ token is still on the agent plane.
Install
npm install -g @hyperdrive.bot/airlock-cli
airlock auth login --domain <your-tenant-domain> # e.g. acme.airlock.it.com--domain is not optional in practice. The CLI resolves its bootstrap URL in
this order: AIRLOCK_BOOTSTRAP_URL → ~/.airlock/config.json → the tenant domain
you pass → a baked-in default. That baked-in default points at dev/us-east-1,
so a bare airlock auth login against a live tenant talks to the wrong
environment. Passing --domain is what makes the third rule fire, and it is the
only step a new install needs.
If you would rather pin it once instead of passing the flag every time:
airlock auth init --bootstrap-url https://<your-tenant-domain>/tenant/bootstrapHuman workflow
airlock auth login --domain <your-tenant-domain> # Cognito PKCE (opens a browser)
airlock credentials set prod/stripe --value sk_live_…
airlock credentials list
airlock get prod/stripe # direct reveal (audited)
airlock credentials rotate prod/stripe --value sk_live_new…Approve / deny agent requests:
airlock approve req_01H… --otp 123456
airlock deny req_01H…Manage agent tokens:
airlock agent issue --name ci-bot --scope "prod/*" # raw token shown ONCE
airlock agent list
airlock agent revoke agt_01H…Bulk-import from 1Password (via the op CLI):
op signin # a 1Password session is required
airlock credentials import --vault Engineering --dry-run # preview the plan (values masked)
airlock credentials import --vault Engineering --prefix prod --yes # create themEach 1Password item maps to a credential: name ← item title, path ← [prefix/]<slug(title)>,
value ← the item's password/credential field (auto-detected, or pick one with --field <id>),
tags ← the item's 1Password tags, policyTier ← --tier (default always-approve). Use
--filter-tag to import only tagged items. Secret values are never printed, logged, or written to disk.
Agent workflow: airlock run (the agent never sees the secret)
An agent uses airlock run. It must never call airlock get.
get reveals plaintext to whoever ran it. That is fine for a human at a
terminal; for an agent it means the secret enters the process, the logs and the
transcript. run exists so it does not have to: the broker starts a local
injecting proxy, the child process is handed a localhost base URL, and the
real credential is attached only on the proxy→upstream leg.
export AIRLOCK_TOKEN=alk_agt_…
airlock run stripe-charge --arg amount=4200 --reason "refund #1234"
# → creates an access request, notifies a human approver, waits for approval,
# then runs the verb's command with the secret injected ON THE WIRE.
# The child gets a localhost URL, never the bytes. No standing access.The command itself lives in the verb, not on your command line: the verb's
command[] is templated with {{base}} (the proxy URL) and {{yourArg}}
placeholders, which --arg key=value fills in. That is deliberate. If the caller
could supply an arbitrary command, the caller could choose where the credential
goes.
The secret is never in the child's environment, argv, or stdout. That is
enforced, not aspirational: npm run test:smoke asserts it on every change.
Verbs are authored by a human, on the server
A verb binds secretRef → upstream → injection. It is registered human-plane
and stored server-side, and a hash of the binding is frozen into the single-use
grant. The proxy injects from that pin, so an agent cannot repoint a verb at
its own collector and have a real secret sent there. Tampering fails closed.
airlock verb add stripe-charge --preset stripe --secret prod/stripe # human
airlock verb ls # read-only
airlock verb add --list-presets # anthropic, github, gitlab, openai, sendgrid, slack, stripeInjection modes cover more than bearer headers: query params, JSON body fields, HMAC request signing, env vars, secret-as-file, mTLS client identity, and an ssh-agent broker where the child can sign but never read the key.
Transparent mode: let the hook do it
You do not have to type airlock run at all. Install the Claude Code hook and
matching commands are routed through the broker automatically:
airlock hook install # --dry-run to preview, --uninstall to removeIt rewrites a command only when exactly one verb is pinned to the host that command targets. No match, several hosts, or two verbs claiming the same host all leave the command untouched, on purpose: an unwrapped command fails loudly and recoverably, whereas a wrongly wrapped one sends a live secret somewhere nobody chose. The install is idempotent, backs up your settings first, and appends alongside any hook you already have rather than replacing it.
A /airlock skill ships in the package too, stating the invariant an agent has to
respect: use run, never get, and on denial report and stop.
airlock get (human plane only)
airlock get prod/stripe # direct reveal, auditedProving it yourself
The claim is that a credential can be used without ever being seen. Three automated proofs assert it on every change, against a real proxy and a real child process:
npm run test:smoke # no-leak (verb path) + daemon + no-leak (passthrough)Those use a sentinel, so they never touch a real vault. To prove the whole chain against live infrastructure, including the reveal and the grant:
airlock auth login --domain <your-tenant>
# from a clone of this repo:
node scripts/live-proof.mjs <credential-path-or-credId>
# or straight from the installed package:
node "$(npm root -g)/@hyperdrive.bot/airlock-cli/scripts/live-proof.mjs" <credential-path-or-credId>It registers a throwaway verb pinned to a local upstream, runs a real command through the broker, and asserts the credential reached the upstream and appears in none of the captured stdout, stderr, argv or environment. The upstream receives the value and performs that check itself, printing only booleans, so running the proof cannot leak what it is proving.
Worth knowing why it exists: the sentinel proofs construct the secret locally, so
they all passed while a verb whose secretRef was a credential path 404'd on
every real run. Only the live proof caught that.
Commands from plugins (do not hand-write)
auth login/logout/status/refresh, init, config get/set/show, and
domain current/list/switch come from @hyperdrive.bot/auth-plugin. Generic
vault verbs are also reachable as dynamic manifest commands via
@hyperdrive.bot/cli-api-plugin.
Environment
| Var | Purpose |
|---|---|
| AIRLOCK_TOKEN | Agent token (alk_agt_…). Presence + prefix selects the agent plane. |
| AIRLOCK_API_URL | Override the API base URL (agent plane + human plane). |
| AIRLOCK_AWS_REGION | Override the AWS region for SigV4 signing. |
Build
npm install
npm run build # tsc -b + oclif manifest
./bin/run.js --help