TitanwardenBack to the front page

The guide ยท use, never see

A secret broker for machines where AI agents work

What makes it different from a password manager, how secrets are protected, how to install it on your own machine, what is planned, and what it does not promise yet.

Titanwarden in one minute

Titanwarden is a secret broker for machines where AI agents do work. An agent never reads a secret. It names the secret it needs and the command it wants to run, and the broker starts that command itself with the value injected into it. The agent-facing protocol has no operation that returns plaintext, so an agent cannot leak a value it never received.

It runs as one small Python service on the machine (a launchd agent on macOS, a systemd unit on Linux), keeps everything encrypted in one SQLite file, and asks a human only for the things only a human should do: the first setup, unsealing after a restart, and reading a value on purpose.

tw run --env TOKEN=proj/github -- git push

writes the audit row first

starts the command with the value injected

output passes through the redactor

Agent or script

Broker
checks token, process ancestry, grant

Audit log
hash chained

Child process
git push

What makes it different from a password manager

A password manager such as Bitwarden is built for a person: unlock, read, paste. Let an agent use it and the value lands in the agent's terminal, its transcript, its logs and the next tool call. One shared unlock then serves every agent on the machine, so one careless pane exposes everything.

Titanwarden turns that around:

Titanwarden

twadmin unseal, once per restart

tw run --env NAME=path -- command

Broker injects into the command
agent sees only redacted output

Password manager

bw unlock

bw get name

Plaintext in the agent's hands
terminal, transcript, logs, next paste

How a secret is protected

At rest, the store is envelope encryption with standard parts from one maintained library (pyca cryptography): AES-256-GCM for every value, a random 256-bit master key held only in memory while unsealed, Argon2id for the passphrase (1 GiB memory, 3 passes), HKDF-SHA256 for subkeys. Each secret version has its own data key, wrapped by a keyring key, wrapped by the master key. Every ciphertext is bound to its exact row, so a copied cell cannot be replayed elsewhere.

In operation, the broker has states, and only one of them moves values:

Backups run nightly through SQLite's online backup API and contain ciphertext plus a names-only manifest. A backup opens only with the passphrase or the recovery key.

Argon2id

HKDF

unwrap

unwrap

wraps

wraps one per version

AES-256-GCM

States

twadmin unseal

restart, deploy, crash

twadmin freeze

twadmin unfreeze

panic-seal or audit failure

lockdown-clear with passphrase

Sealed

Unsealed

Frozen

Lockdown

Passphrase, 30+ characters

Slot key

Recovery key, 32 random bytes, shown once

Slot key

Master key
memory only, never on disk

Keyring key

Data key

Encrypted value
bound to its row

Install it on your own machine

You need macOS or Linux, Python 3.12 or newer, uv, and SSH access from a device you trust (the setup commands refuse to run inside an agent pane, and the recovery key is printed once on your own terminal).

git clone <the titanwarden repo> && cd titanwarden
uv sync --frozen
uv run python -m unittest discover -s tests -v     # optional, uses a throwaway TW_HOME under /tmp
scripts/install.sh

install.sh creates the home (~/.titanwarden on macOS, /data/titanwarden on Linux, or TW_HOME if set) with state, run, tokens, panes, log, backups and releases at mode 700, builds a release, links tw and twadmin into ~/bin, and loads three jobs: the broker, a 60-second watch, and a 03:17 backup. On Linux the units are system units and /etc/profile.d/titanwarden.sh exports TW_HOME for SSH sessions.

Then the human ceremony, over SSH from your own device:

twadmin init        # passphrase twice (at least 30 characters), prints the recovery key once: record it offline
twadmin unseal      # passphrase; the broker now serves agents
tw status           # broker version, seal state, your principal

Every restart returns the broker to sealed. After a reboot, a deploy, or a crash: twadmin unseal, and if the web console is enabled, tw.console_setup again.

reboot, deploy, crash

scripts/install.sh

Home, release, tw and twadmin on PATH,
broker + watch + backup jobs loaded

twadmin init over SSH
passphrase twice, recovery key once

twadmin unseal

tw status: unsealed

Agents can tw run

Sealed again: twadmin unseal

Give an agent a credential

Put a value in without typing it into an agent pane. A human uses twadmin put PATH (hidden prompt). An agent with a put grant pipes it in or has the broker generate it, so the value never appears in a command line or a response:

tw put infra/railway-token            # value on stdin; refuses a terminal unless --prompt
tw put infra/db-password --generate 32 --charset urlsafe
tw names infra                        # names, class, version, length bucket: never values

Use it. Prefer a file descriptor or stdin over an environment variable when the tool allows it; a same-user process can read another process's environment, it cannot read a pipe it was not handed.

tw run --env GITHUB_TOKEN=infra/github -- gh pr create ...
tw run --fd 3=infra/db-password -- psql --passfile /dev/fd/3 ...
tw run --stdin infra/api-key -- some-tool --key-stdin
tw inject --manifest .env.tw -- npm start      # .env.tw lines: VAR=tw://project/name, or VAR=literal

A manifest holds references, not values, so it is safe to commit. When an agent lacks a grant it asks: tw request access infra/github --ops inject --reason "release 0.2.0". The request goes to the human with a recommended default; nothing moves until it is approved. Agents can also hand a worker a narrower, time-limited token (tw task-token issue) and kill their own token after a suspected leak (tw token revoke-self).

no

approved

yes

Human: twadmin put, or an agent: tw put / --generate

Store: versioned, encrypted

Agent: tw run --env NAME=path -- command

Grant covers path and op?

tw request access: an ask with a recommended default
human approves or denies

Audit intent committed

Child starts with the value
env, fd or stdin

Output back through the redactor
matches become tw:redacted

The web console

The console is the human face. The slice that ships today is read-only: sign in with a passkey and see the vault state, the secret names, their versions and their last use. Approving requests and revealing a value with a reason are later slices; until then those stay on the command line (twadmin request, twadmin reveal over SSH). The broker serves the console on 127.0.0.1:8787 only; on a tailnet it is published with tailscale serve, which gives it an HTTPS name that passkeys (WebAuthn) require. A forged header to the local port does not log anyone in. After every unseal, tw.console_setup reactivates it.

The questions people ask

Are there one-offs (one-time shares)? Designed, not built yet. The spec is the agent-first successor of Bitwarden Send: one secret per link, encrypted with a key that rides in the URL fragment so the relay never sees plaintext, sent through a dedicated mail account, revealed once and burned, with a copy-and-confirm step that writes a timestamped receipt you can show a client. Scheduled for v1.

Does it sync? Today one broker serves one machine. Two paths exist in the design: an opt-in cloud broker, where a broker enrolls with an organization console (enrollment code, six matching words checked out of band, passkey approval) and exchanges only ciphertext and signed policy; and, later, a tailnet transport so other hosts call one broker with mutual TLS. Neither puts plaintext anywhere but the unsealed broker's memory.

Will it sync with 1Password or Bitwarden? No two-way sync, by design: two sources of truth is how secrets drift. There is a one-way import from Bitwarden, run by a human from a names-only manifest, for moving an item in. 1Password is not in the plan. The flow it does have is outward: Titanwarden as the source of truth that pushes a value to Railway, GitHub Actions, Cloudflare or Coolify (placements, v1) and rotates it end to end.

Can I read a value myself? Yes, on the command line: twadmin reveal over SSH, with your passphrase, a reason, and an audit row. It refuses to run inside an agent pane. The web console cannot reveal yet.

Honest limits today

The design labels every guarantee as a boundary or a guardrail, and the current version is clear about which is which:

The planned next version closes the first gap: the broker runs as its own user, a separate exec runner starts children, and auto-unseal removes the human from routine restarts.

Planned

socket, group-restricted

Agent, user sem

Broker, user _titanwarden

tw-execd starts children

Today

Agent

same OS user

Broker

Children

When something fails

Every Titanwarden message on stderr starts with tw: and never contains a value. The exit code tells you which layer refused:

ExitMeaningWhat to do
64bad arguments or framefix the command
69broker unreachableis the job loaded? tw status, then the watch log
70integrity or audit failurethe store or its log failed verification; stop and check backups
75sealed, frozen or busytwadmin unseal, twadmin unfreeze, or wait
77refused: auth, scope, policy, identity mismatchrequest a grant, or check which token file this process found
78no identitythe caller has no token file

A denial never says whether a path exists. The caller learns only "denied".