All documentation

Developer guide

Every command you need, in the order you need them.

Everything you need to use Warder day to day, in the order you need it.

This assumes Warder is already running somewhere and somebody has given you its address. You do not need to deploy anything.


Installing the CLI

Developers who are not working on Warder itself should not have to clone it and have a Go toolchain to get the CLI. Three ways in, in the order most people want them.

The install script

curl -fsSL https://raw.githubusercontent.com/Tobe0504/Warder/main/install.sh | sh

Detects the platform, downloads the matching build from the GitHub release, verifies its SHA-256 against the published checksums, and installs to the first writable directory on PATH. Pin a version or choose a location:

WARD_VERSION=v0.2.0 WARD_INSTALL_DIR="$HOME/.local/bin" ./install.sh

The checksum check is not decoration. This binary ends up holding a credential that reaches every secret its identity is granted, so a tampered download is not a cosmetic problem. The script refuses to install on a mismatch.

Piping a script into a shell is a real thing to be uneasy about. If your team's policy says no, download it, read it, then run it; it is a hundred lines of POSIX sh and does nothing but fetch, verify, and move a file.

With Go

go install github.com/Tobe0504/Warder/cmd/ward@latest

Lands in $(go env GOPATH)/bin. The version reports as 0.1.0-dev, because the real tag is stamped by the release build rather than compiled in.

Straight from a release

Download the archive for the platform from the releases page, check it against checksums.txt, extract, and put ward somewhere on PATH.


Daily development

Once, in each repository that needs secrets:

ward init --project payments-api --env development

This writes .warder.json naming the project and environment. Commit it. It holds no credentials: only two names, so everyone on the team gets the same target without passing flags.

Once per machine:

ward login

Then, instead of whatever you ran before:

ward run -- npm run dev

That is the whole workflow. The secrets reach the process; they do not reach your terminal, your shell history, or a file.

ward run -- npm test
ward run -- python manage.py migrate
ward run --env staging -- ./deploy.sh

Ask for less

ward run --key DATABASE_URL --key REDIS_URL -- npm test

Naming the keys means the process holds only what it actually uses. If one of them is not available to you, the command stops before starting anything rather than failing confusingly ten seconds later.

See what you have

ward status                 # who you are signed in as
ward project list
ward secret list            # names, versions, expiry: never values

ward secret list shows a VALUE column full of dots on purpose. You can see that DATABASE_URL exists, that it is at v7, and that it expires in fourteen days. Seeing what it is requires READ_SECRET, which no role grants, and it happens in the dashboard where it can be recorded against a person.


Adding and rotating secrets

In the dashboard: Projects → your project → Secrets → Add secrets.

Values are encrypted before they are stored. You will not see one again unless someone explicitly grants you permission to reveal it, including if you are the person who typed it in.

Importing a .env file

The same dialog takes a whole file. Paste one into any key field and the rows fill in: export prefixes, comments, quotes, trailing notes, and values spanning several lines are all understood. Pick the environment once at the top and everything in the batch goes there.

Only the key field does this. A value field takes what you paste literally, because that is exactly where a raw credential goes, and a connection string or a PEM block scattered across rows would be worse than no help at all.

The whole import is one transaction. If any key is malformed or already exists, nothing is stored: an environment holding half a configuration is worse than one holding none, because an application will boot on it.

Lines that could not be read are named rather than dropped, so a credential never goes missing quietly. Each secret still gets its own audit event: "twenty secrets created" would not answer "when did STRIPE_KEY appear".

A key that already exists is refused rather than overwritten. Replacing a value is a rotation, and rotation is a separate, deliberate act.

Rotating

Rotate on the secret's row. A new version becomes active immediately, and applications pick it up on their next start. Nothing they hold changes: same project, same environment, same key.

Rotating here changes the value Warder stores. It does not change the credential at the provider. Rotate it at the provider first: generate the new database password, the new API key, then paste the new value here. The dialog says this too, because assuming otherwise leaves a live credential in circulation.

Expiry

Set Expires on any version. After it passes, runtimes stop receiving it and the dashboard shows EXPIRED. Useful for a credential you know is temporary; a loud failure beats a credential that quietly outlives its purpose.


Giving an application access

Three steps, and the order matters.

1. Create an identity, Identities → New identity

One per thing that runs your code: the API in production, the CI job, an agent. Separate identities mean you can revoke one without disturbing the others. A new identity holds no access at all.

2. Grant it access, your project → Access → Grant access

Choose the identity, the environment, and the capabilities. Two checkboxes:

  • Can use: a runtime receives the value and injects it into a process.
  • Can see: a person can display the plaintext in the dashboard.

Applications want the first. They almost never want the second.

3. Issue it a token, your project → Tokens → New token

The token is shown once. Only a verifier is stored, so it cannot be shown again: if it is lost, revoke it and issue another.

Then, in the runtime's own environment:

WARDER_RUNTIME_URL=http://127.0.0.1:8081
WARDER_TOKEN=vlt_...
ward run -- ./server

Same command as a developer runs. The credential comes from the environment instead of a login.

Answering "what happens if I revoke this?"

Immediately. Revoking a token also revokes every short-lived session already issued from it, so the next request is denied rather than the one after the session would have expired.


Onboarding an AI agent

An agent is not you. It reads untrusted input, issues, pull requests, dependency READMEs, and can be talked into things. Give it its own identity.

Identities → New identity → AI agent, and set an expiry. The session ends by itself; nobody has to remember to clean it up.

Then grant it development, can use only, and issue a token scoped to the keys it needs:

WARDER_RUNTIME_URL=http://127.0.0.1:8081
WARDER_TOKEN=vlt_...

The agent can now run:

ward run -- npm test

and cannot print a credential, reach staging or production, rotate anything, or change who has access. The dashboard API refuses its token outright.

What this does not do: ward run -- npm test starts a process whose environment holds the test credentials, and the agent can read that process's environment. The protection is that those are test credentials, in development, scoped as narrowly as you chose: not that the agent is prevented from reading what it was authorized to use. See limitations.


CI and containers

Create a CI identity, grant it the environment it deploys to, issue a token, and store that token in your CI system's secret store. Then:

- run: ward run -- ./deploy.sh
  env:
    WARDER_RUNTIME_URL: ${{ vars.WARDER_RUNTIME_URL }}
    WARDER_TOKEN: ${{ secrets.WARDER_TOKEN }}

One credential in your CI system instead of fifteen, and it is scoped to one project and one environment. Rotating a database password no longer means editing CI configuration.

In a container, set the same two variables and use ward run as the entrypoint:

ENTRYPOINT ["ward", "run", "--"]
CMD ["./server"]

Onboarding and offboarding people

Adding someone: Members → Invite member. Give their name, address and role, and set an expiry if they are a contractor. You get a single-use link to send them; nothing is emailed from Warder.

The link works once and lapses after seven days. Until it is used you can withdraw it from the same page, which is what to do if it went to the wrong place.

Warder never asks you to choose somebody else's password. The invitee sets their own when they open the link, so you do not learn it, you never have to send a credential over chat, and you cannot sign in as them afterwards. The address and the role are fixed when you create the invitation, whoever opens the link joins as exactly that person, at exactly that role, and can change neither.

The role governs administration. It grants no access to any secret value, that is always a separate, explicit grant.

Removing someone: Members, then remove them. One transaction ends three things together:

  • the membership, so no request of theirs resolves to a principal again
  • their grants, so the Access page stops listing people who left
  • their sessions, so nothing already in flight survives

Their password is still correct and now confers nothing. No credential rotates, because they never held one: they could use DATABASE_URL, and using it was always Warder fetching it on their behalf.

No credential needs rotating. That is the point. Someone who could use DATABASE_URL never held it, so their leaving does not put it at risk.


Troubleshooting

ward: not logged in

ward login

Sessions expire. This is the ordinary case.

ward: could not reach the service

WARDER_RUNTIME_URL is unset, wrong, or the deployment is down. Check the address with whoever operates it:

echo $WARDER_RUNTIME_URL
curl -s "$WARDER_RUNTIME_URL/health"

{"status":"ok"} means the service is up and the problem is your session or your project, not the address.

ward: no project "..."

The name in .warder.json does not exist, or exists in an organization you are not a member of. ward project list shows what you can actually see.

ward run starts but the process has no secrets

It authenticated and was granted nothing. The line above the command names what it was refused. An identity needs an explicit Can use grant on that environment: being a member of the organization is not enough, and no role confers it.

A secret shows EXPIRED

Its version passed the expiry someone set. Rotate it, or ask whoever owns it to clear the expiry.

That email and password do not match an account

Exactly what it says, and it does not distinguish which half was wrong. If you have never signed in to this deployment, you may not have an account on it yet: ask for an invitation.


Command reference

ward

CommandWhat it does
ward init --project P --env EWrite .warder.json, commit it
ward loginSign in and store a session at mode 0600
ward logoutRevoke the session and remove it locally
ward statusWho you are signed in as
ward project listProjects you can see
ward environment listEnvironments in a project
ward secret listSecret names, versions, expiry: never values
ward run -- <cmd>Run with authorized secrets injected

Flags for ward run:

FlagEffect
--projectOverride .warder.json
--envOverride .warder.json
--key KEYRequest only this secret; repeatable
--quietSuppress the summary line

Environment variables

For a runtime, instead of ward login:

VariablePurpose
WARDER_RUNTIME_URLThe runtime API address
WARDER_TOKENThe machine token

ward run removes WARDER_TOKEN from the child process's environment. The child needs the secrets, not the ability to ask for more of them.