# seekrit — full documentation > Generated from https://seekrit.dev/docs. Zero-knowledge secrets manager; built for humans and AI agents. --- # Introduction **seekrit** is an end-to-end encrypted, multi-tenant secrets manager built on Cloudflare Workers. It stores your API keys, database URLs, and other secrets, and hands them to your applications — in local development, Docker builds, CI, Kubernetes, and AI-agent sandboxes — without the server ever seeing a plaintext value. > **Note:** **Building with an AI agent?** Start with the [AI agents guide](/docs/guides/ai-agents), or point any agent at the machine-readable docs: [`/llms.txt`](/llms.txt) (index), [`/llms-full.txt`](/llms-full.txt) (everything in one file), or any page's URL with a `.md` suffix. Setup is one command: `claude mcp add seekrit -- seekrit mcp`. ## Zero-knowledge by design The defining property of seekrit is that **encryption keys are generated and used entirely on the client**. When you add a secret in the web dashboard or CLI, it is encrypted in your browser or on your machine before it is uploaded. The API — a Cloudflare Worker backed by D1 — only ever stores ciphertext. A full database dump reveals metadata (names, versions, timestamps) but no secret values. This is achieved with **envelope encryption**: - Each **environment** has its own AES-256 data key (DEK). - Secrets are encrypted with that DEK. - The DEK is **wrapped** (encrypted) individually to each person or service token that should have access, using their public key. - A user's private key is itself encrypted with a passphrase-derived key, so any device can fetch it and unlock it locally — the passphrase never leaves the client. See [Encryption model](/docs/concepts/encryption) for the full picture. ## The pieces seekrit is a small monorepo: - **Web dashboard** — create orgs, applications, and environments, and manage secrets with client-side encryption. Sign in with Google or GitHub (via Stytch). - **CLI** (`seekrit`) — link a project directory to an environment and inject decrypted secrets into any command, or export them as `dotenv`/`json`/`shell`. - **API** — a Hono Worker on Cloudflare with D1 (database) and KV, exposing an org-scoped REST API and an append-only audit trail. ## Where to go next --- # Quickstart This walkthrough runs the whole stack on your machine and encrypts your first secret with the CLI. It uses local development auth (no Stytch project required). > **Note:** seekrit runs entirely on your Cloudflare account in production, but every part runs locally for development — no cloud resources needed to try it. ## Prerequisites - Node.js ≥ 22 and pnpm ≥ 10 - A clone of the repository ## 1. Install and build ```bash pnpm install pnpm build ``` ## 2. Start the API The API is a Cloudflare Worker. In development it runs against a local D1 database and a local KV namespace. ```bash cd apps/api cp .dev.vars.example .dev.vars # enables AUTH_MODE=dev pnpm db:migrate:local # apply migrations to the local D1 pnpm dev # http://localhost:8787 ``` `AUTH_MODE=dev` lets you authenticate with a simple `x-seekrit-dev-user` header instead of a real identity provider. It only works locally and is never enabled in production. ## 3. Encrypt a secret with the CLI In a second terminal, from the repository root: ```bash # Point the CLI at the local API as a dev user alias seekrit="node $PWD/apps/cli/dist/index.js" export SEEKRIT_PASSPHRASE=dev-only-passphrase seekrit login --dev-user you@example.com --api-url http://localhost:8787 seekrit keys setup # generate your keypair (client-side) seekrit org create --name "Acme" --slug acme seekrit app create --org acme --name "Storefront" --slug storefront seekrit env create --org acme --app storefront --name Production --slug production seekrit init --org acme --app storefront # writes seekrit.json (org + app; no env) seekrit secrets set DATABASE_URL 'postgres://user:pass@host/db' --env production ``` > **Note:** `seekrit.json` names the default org and app but **never an environment** — so it's safe to commit and runs everywhere. The environment is chosen at runtime by a service token (or, interactively, the `--env` flag). > **Tip:** `seekrit keys setup` generates a P-256 keypair in the CLI and uploads only your public key and a passphrase-encrypted copy of your private key. Setting `SEEKRIT_PASSPHRASE` avoids the interactive prompt — omit it and you'll be asked. ## 4. Use your secrets Inject the resolved environment into any process, or print it: ```bash seekrit run --app storefront --env production -- printenv DATABASE_URL seekrit export --app storefront --env production --format dotenv seekrit secrets list --app storefront --env production ``` For a deployed app you'd instead mint a token bound to the environment, and it selects everything at runtime — no flags: ```bash TOKEN=$(seekrit token create --name local-dev --app storefront --env production) SEEKRIT_TOKEN=$TOKEN seekrit run -- printenv DATABASE_URL ``` Everything you just did was encrypted on your machine. The API only ever received ciphertext. ## 5. Try the web dashboard (optional) ```bash cd apps/web cp .env.example .env.local pnpm dev # http://localhost:3000 ``` Sign in with the dev identity form, set up your keys, and manage the same orgs and secrets in the browser. To enable Google/GitHub sign-in, see the [Web dashboard guide](/docs/guides/web-app). ## Next steps - Understand the [encryption model](/docs/concepts/encryption) - Create [service tokens](/docs/guides/service-tokens) for CI and containers - Browse the [CLI command reference](/docs/reference/cli) --- # Encryption model seekrit uses **envelope encryption**. There are three layers of keys, and the plaintext of your secrets — along with private keys and passphrases — never reaches the server. ## The key hierarchy ``` environment DEK (AES-256) one per environment ├── encrypts every secret in that environment └── wrapped separately for each principal: ├── user A (ECDH to A's public key) ├── user B (ECDH to B's public key) └── token T (ECDH to T's public key) ``` ### Data encryption key (DEK) Every environment has its own random 256-bit **data encryption key**. Secrets in that environment are encrypted with it using **AES-256-GCM**. The ciphertext is bound to its location with additional authenticated data (AAD) of `environmentId/SECRET_NAME`, so a blob cannot be silently moved to a different secret or environment. ### Principal keypairs Every principal — a user or a service token — has a **P-256 (ECDH) keypair**. The public key is stored by the server; the private key is not (for tokens it lives inside the token string; for users it is passphrase-encrypted, see below). ### Key wrapping To give a principal access to an environment, its DEK is **wrapped** to that principal's public key. Wrapping uses an ephemeral ECDH exchange plus HKDF-SHA256 to derive a one-time AES-256-GCM key — an ECIES-style construction. Only the holder of the matching private key can unwrap the DEK. Each grant is an independent wrapped copy of the same DEK. > **Note:** Possession of a wrapped-DEK grant **and** the matching private key is exactly what "having access" means. There is no server-side switch that grants plaintext — access is cryptographic. ## Protecting user private keys A user needs their private key on every device they sign in from, but the server must never see it. So the private key is encrypted client-side with a key derived from the user's **passphrase** using PBKDF2-HMAC-SHA256 (600,000 iterations), and the encrypted blob is stored server-side. - On any device, the client fetches the encrypted blob and decrypts it locally with the passphrase. - The passphrase and the plaintext private key never leave the client. - A wrong passphrase surfaces as an authentication failure — the blob simply won't decrypt. There is intentionally **no passphrase reset**: losing it means the encrypted private key is unrecoverable. That is the cost of the server never holding it. ## Service tokens Service tokens are self-contained principals for machines. The token string itself carries the private key. The server stores only: - a SHA-256 hash of the full token (to authenticate requests), and - the token's public key (to wrap DEK grants to it). So a machine holding the token can unwrap any environment DEK granted to it, entirely offline, without the server ever having its private key. See [Service tokens](/docs/guides/service-tokens). ## What a secret write looks like 1. The client fetches its wrapped DEK for the environment and unwraps it with its private key. 2. It encrypts the new value with the DEK (AES-256-GCM, AAD = `envId/NAME`). 3. It uploads only the resulting ciphertext blob. The reverse — fetch ciphertext, unwrap DEK, decrypt — happens on read. The API is never a party to any of the cryptography. ## Algorithms at a glance | Purpose | Algorithm | | --- | --- | | Secret encryption | AES-256-GCM (per-secret AAD) | | DEK wrapping | Ephemeral ECDH P-256 + HKDF-SHA256 → AES-256-GCM | | Passphrase key derivation | PBKDF2-HMAC-SHA256, 600k iterations | | Service token hashing | SHA-256 | All of it runs on the Web Crypto API, so the exact same code runs in the browser, the Workers runtime, and Node for the CLI. Every ciphertext blob is versioned (e.g. `sc1.`, `wd1.`, `pk1.`) so algorithms can be migrated without breaking existing data. --- # Architecture ## Resource model seekrit is multi-tenant. Resources nest from organizations down to individual secrets: ``` organizations ─┬─ members (users, via org_memberships with a role) ├─ groups ──────── environments ─┬─ secrets (+ version history) ├─ applications ─── environments ─┤ (shared, reusable secret bags) │ └─ composes groups └─ key grants (wrapped DEKs) ├─ service tokens (bound to one app environment) └─ audit log (append-only) ``` - **Organization** — a tenant. Has members, applications, groups, service tokens, and its own audit trail. - **Application** — a deployable (a service, site, or worker) within an org. - **Group** — a reusable secret bag shared across applications (e.g. `common-backend`, `auth-providers`). Like an application, it owns environments keyed by slug. - **Environment** — a named context (`production`, `staging`, `dev`, …) owned by **either** an application or a group. Each environment owns one data key. - **Composition** — an application environment pulls in one or more groups, matched by slug. At read time the layers merge, lowest precedence first: `group secrets < app secrets`. - **Secret** — a named, encrypted value in an environment. Every write appends a new version. - **Key grant** — a wrapped copy of an environment's data key for one principal. - **Service token** — bound to one application environment; that binding selects the org, app, and environment it resolves at runtime, plus the group slices composed into it. ## Roles Membership carries a role: `owner` > `admin` > `member`. - **Admin and owner** manage structure (apps, environments), service tokens, key grants, and can read the audit trail. - **Members** read and write secrets in environments they hold a key for. The real access boundary is cryptographic: you can only decrypt an environment if you hold a key grant for it. Roles gate the management API on top of that. Non-members receive `404`s for an org so its existence can't be probed. ## Cloudflare building blocks seekrit is designed to run entirely on a Cloudflare account. | Component | Cloudflare product | Role | | --- | --- | --- | | API | Workers | The Hono API worker | | Database | D1 (SQLite) | Orgs, users, secrets (ciphertext), grants, audit | | Cache | KV | Cached identity-provider JWKS (and future session/rate-limit state) | | Web dashboard | Workers (via OpenNext) | Next.js app serving the browser client | The API and web app deploy as separate Workers. D1 and KV are provisioned as bindings; the CLI `wrangler` handles migrations and deploys. ## Request lifecycle 1. A request arrives at the API worker with a credential — a Stytch session JWT, a service token, or (locally) a dev-user header. 2. Auth middleware resolves the **actor** (a user or a service token) and, for org-scoped routes, checks membership and role. 3. The handler reads or writes ciphertext in D1. It never handles plaintext secrets. 4. Any mutating action writes an entry to the append-only audit log before the response returns. High-volume environment resolves are metered for usage/billing instead of audited per call (denied resolves are still audited). ## Authentication - **Web** — Stytch B2B sign-in (Google/GitHub OAuth discovery). The browser holds the session; its JWT is sent as a bearer token and verified by the API against Stytch's JWKS (cached in KV). - **CLI / machines** — service tokens (`skt_…`) sent as bearer tokens. - **Local dev** — an `x-seekrit-dev-user` header, enabled only when the API runs with `AUTH_MODE=dev`. The first time a valid session proves access to a Stytch organization, seekrit provisions the matching org and membership just-in-time. --- # Access & key grants Access to an environment's secrets is **cryptographic**: a principal can decrypt an environment if and only if it holds a key grant — a copy of the environment's data key (DEK) wrapped to its public key — and the matching private key. ## Granting access When a principal is granted access, the granting client: 1. Unwraps its own copy of the environment DEK with its private key. 2. Re-wraps that DEK to the target principal's public key. 3. Uploads the new wrapped blob as a key grant. All of this happens on the granter's client. The server stores the wrapped blob but never sees the DEK in the clear. Because wrapping needs the target's public key, a user must have completed [key setup](/docs/guides/web-app) before they can be granted access. In the web dashboard this is the **Key access** panel on an environment; with the CLI it is `seekrit grant`. ## Roles vs. grants Two independent mechanisms govern what someone can do: - **Role** (`owner` / `admin` / `member`) gates the management API — who may create environments, mint tokens, manage grants, or read the audit trail. - **Key grant** gates decryption — who can actually read secret values. A member with no key grant for an environment can see that the environment exists (if their role allows) but cannot decrypt anything in it. Service tokens carry a role too: **member** (the default — a runtime credential whose real power is its key grants) or **admin** (an org-scoped token that also passes the management API, so automation and [AI agents](/docs/guides/ai-agents) can provision structure headlessly). A token is never `owner`. Only an admin caller can mint an admin token, and granting any token decryption still requires the caller to already hold that environment's key — so capability never escalates itself. ## Revocation vs. rotation These solve different problems, and both matter. **Revocation** deletes a principal's key grant. It immediately stops that principal from fetching the DEK again. Use it when someone leaves or a token is retired. > **Warning:** Revoking a grant does not change the DEK. A principal that already fetched and cached the DEK could still decrypt values it captured. To truly cut off access to existing secrets, rotate the key. **Rotation** replaces the environment's DEK entirely: generate a new DEK, re-encrypt every secret with it, and re-wrap it for the remaining principals. After rotation, any previously cached copy of the old DEK is useless. Revocation is instant and cheap; rotation is the complete, heavier operation. A typical response to a departing teammate is to revoke immediately, then rotate. ## Auditing access changes Every grant and revocation is recorded in the [audit trail](/docs/concepts/security#audit-trail) with the actor and target principal, so access changes are always attributable. --- # Security model seekrit's guarantee is simple: **the server never has the information needed to read your secrets.** This page states that precisely. ## What the server stores - Secret **ciphertext** (AES-256-GCM blobs). - Environment DEKs, but only **wrapped** to principals' public keys — never in the clear. - Users' **public keys**, and their **private keys only in passphrase-encrypted form**. - For service tokens: a **SHA-256 hash** of the token and the token's **public key**. - Metadata: org/app/environment/secret **names**, versions, timestamps, roles, and the audit log. ## What the server never sees - Plaintext secret values. - Any DEK in the clear. - Any user's private key in the clear. - Any passphrase. - The full service token string (only its hash). ## Threat model **A full database compromise** yields ciphertext and metadata. An attacker learns that `STRIPE_KEY` exists in `acme/storefront/production` and when it changed, but not its value. To decrypt, they would additionally need a principal's private key, which the database does not contain. **A compromised or malicious server** can deny service, tamper with metadata, or serve altered ciphertext — but AES-GCM authentication means tampered ciphertext fails to decrypt rather than producing wrong plaintext silently. It still cannot read secret values, because it never holds the keys. **A leaked passphrase** exposes that user's private key (and therefore every environment granted to them). Treat passphrases like the master credential they are. **A leaked service token** grants whatever environments were wrapped to it. Revoke it; rotate the affected environment keys. > **Note:** Names are metadata, not secrets. Don't encode sensitive information in org, application, environment, or secret **names** — only in secret **values**. ## Audit trail Every write, grant, revocation, and token action is recorded in an append-only audit log, attributed to the acting user or service token, with contextual metadata (never secret material). The API exposes no update or delete path for audit entries. This gives you an after-the-fact record of who touched what, even though the server can't read the secrets themselves. High-volume environment resolves (`GET /v1/resolve`, called on every app or CI startup) are the deliberate exception: they are metered for usage and billing rather than written to the audit log per call, so the log stays a signal of change and access-control events, not a firehose of identical machine reads. A *denied* resolve — a principal without a key grant — is still audited. ## Transport and authentication - All API traffic is authenticated: a Stytch session JWT (verified locally against Stytch's JWKS), a service token, or a local dev header. - Session JWTs are validated for issuer, audience, and expiry. - Service tokens are matched by hash and checked for revocation and expiry on every request. ## Responsible disclosure seekrit is early-stage software. If you find a vulnerability, please report it privately through the repository's security policy rather than opening a public issue. --- # Web dashboard The web dashboard is a Next.js app where you manage orgs, applications, environments, and secrets. All encryption and decryption happens **in your browser** — the same envelope-encryption scheme the CLI uses. ## Signing in The dashboard supports two ways to authenticate: - **Google / GitHub** via Stytch B2B (recommended). Requires a Stytch project (below). - **Dev identity** — a local-only email form, active when the API runs with `AUTH_MODE=dev`. ### Sign in with Google or GitHub Clicking **Continue with Google/GitHub** starts Stytch's B2B *discovery* OAuth flow: you authenticate with the provider, then choose an existing organization or create a new one, and a session is issued. seekrit provisions the matching org just-in-time on the API's first authenticated request. To enable these buttons, configure a Stytch B2B project once: 1. Create a **B2B** project at [stytch.com](https://stytch.com). 2. Under **OAuth**, enable **Google** and **GitHub**. 3. Under **Redirect URLs**, add `http://localhost:3000/auth/callback` as a **Discovery** URL (and your production URL later). 4. Put the keys in config: - `apps/web/.env.local` → `NEXT_PUBLIC_STYTCH_PUBLIC_TOKEN=public-token-test-…` - `apps/api/.dev.vars` → `STYTCH_SECRET=secret-test-…` - `apps/api/wrangler.jsonc` vars → `STYTCH_PROJECT_ID` and `STYTCH_API_URL` (`https://test.stytch.com` for the Test environment) > **Note:** The web app receives only the Stytch **public** token. The secret key lives with the API worker, which uses it to verify sessions and resolve members. The browser never holds it. ## Key setup The first time you sign in you'll be asked to create your encryption keys. The browser generates a P-256 keypair and encrypts the private key with a passphrase you choose. Only your **public key** and the **passphrase-encrypted** private key are uploaded. > **Warning:** There is no passphrase reset. If you forget it, your encrypted data cannot be recovered. Store it in a password manager. On later visits, the keyring starts **locked**. The first time you reveal or edit a secret in a session, you'll be prompted to unlock it with your passphrase; it stays unlocked (in memory only) until you lock it or reload. ## Managing secrets - **Organizations** → **Applications** → **Environments** — create each from its list page. Creating an environment generates its data key in your browser and wraps it to your public key. - On an environment page, the **Secrets** table lets you add, reveal, edit, and delete secrets. Values are encrypted on save and decrypted on reveal, locally. - Secrets inherited from [composed groups](#groups) appear in the same table under an **inherited from groups** heading, greyed out and tagged with their source group. Click one to jump to the group environment it lives in. A secret you define here shadows an inherited one of the same name, so it's shown as your own (editable) row instead. - The **Key access** panel (admins) shows who holds the environment key and lets you grant it to members or service tokens, or revoke it. - The **Composed groups** panel (admins) layers shared [groups](#groups) beneath an environment's own secrets — see below. ## Groups **Groups** are reusable secret bags shared across applications — a shared database cluster, third-party API keys, anything more than one app needs. Manage them from the **Groups** entry in the org sidebar. - A group has its own **environments**, one per slug you want to share (e.g. `production`, `staging`). Each group environment holds secrets and key grants exactly like an application environment — same client-side crypto, same **Key access** panel. - On an **application** environment's page, the **Composed groups** panel lets admins compose one or more groups. At resolve time the layers merge in order: composed groups first (a group lower in the list overrides one above it), then the environment's own secrets on top. Reorder with the arrows; remove with ✕. - A group is matched into an app environment **by slug**: composing `shared-infra` into an app's `production` environment pulls in the group's `production` environment. Create the matching group environment before you rely on it. > **Note:** Composition wires up *which* secrets layer together, not *who* can read them. Each principal — member or token — still needs its own key grant on every group environment it should decrypt. Minting a token (below) grants those automatically. ## Service tokens & audit - **Service tokens** — mint machine credentials for CI and containers. Each token is **bound to one application environment**: pick the app and environment when minting, and the browser auto-grants the token that environment's key plus the keys of every group it composes (unwrapping each with your key and re-wrapping to the token). The token is shown once; copy it then. See [Service tokens](/docs/guides/service-tokens). - **Audit trail** — every action in the org, append-only, with actor attribution. ## Troubleshooting sign-in If the dashboard shows an error after sign-in, it will name the cause: - **Session rejected (401)** — the API couldn't verify your session. Usually an expired session (sign in again) or a `STYTCH_PROJECT_ID`/secret mismatch on the API. - **API unreachable** — the API worker isn't running or `NEXT_PUBLIC_SEEKRIT_API_URL` is wrong. The API worker logs the underlying reason for verification failures. --- # CLI The `seekrit` CLI injects a decrypted, layered environment into your processes. It decrypts locally using the same keys as the web app — a service token's embedded key, or your passphrase-unlocked private key. At runtime a **service token selects the org, app, and environment**, so `seekrit run` needs nothing else. For an exhaustive list of commands and flags, see the [CLI reference](/docs/reference/cli). ## Authentication The CLI reads credentials from config or environment variables: - `SEEKRIT_TOKEN` — a service token (`skt_…`), for CI and machines. - `SEEKRIT_DEV_USER` — a dev identity (local API with `AUTH_MODE=dev`). - `seekrit login` persists these to `~/.config/seekrit/config.json`. ```bash # machine / CI export SEEKRIT_TOKEN=skt_... export SEEKRIT_API_URL=https://api.your-seekrit.example # or, for local development seekrit login --dev-user you@example.com --api-url http://localhost:8787 ``` ## Linking a project `seekrit init` writes a `seekrit.json` naming the default org and app for management commands. It is **environment-independent** — it never pins an environment — so it's safe to commit and behaves the same everywhere. ```bash seekrit init --org acme --app storefront ``` The environment is chosen at runtime by the service token (or the `--env` flag when you run commands interactively). ## Running with a service token This is the primary path: the token carries its org, app, and environment. ```bash export SEEKRIT_TOKEN=skt_... seekrit run -- ./start-server # resolves & injects; no flags needed seekrit export --format dotenv > .env ``` The resolved environment is **layered**, lowest precedence first: ``` group secrets < app-env secrets < .env file < process env ``` Add `--explain` to see where each variable came from, and `--with =` to swap a single group's slice for one run (see [Environments & groups](/docs/guides/environments)). ## Running interactively (as a user) Without a token, target the environment explicitly: ```bash seekrit run --app storefront --env production -- npm run dev seekrit export --app storefront --env production --format shell ``` ## Reading & writing individual secrets Every `secrets` command targets an environment with `--env` plus `--app` (or `--group`); `--org` comes from `seekrit.json` or a lone org. ```bash seekrit secrets list --app storefront --env production seekrit secrets get STRIPE_KEY --app storefront --env production seekrit secrets set DATABASE_URL 'postgres://…' --app storefront --env production printf '%s' "$TOKEN" | seekrit secrets set GITHUB_TOKEN --app storefront --env production seekrit secrets rm OLD_KEY --group common --env production # a group's secret ``` ## Unlocking (users) When authenticated as a user (not a service token), decryption needs your passphrase to unlock your private key. Set it non-interactively for scripts: ```bash export SEEKRIT_PASSPHRASE='…' ``` Omit it and the CLI prompts. Service tokens don't need a passphrase — their key is in the token. > **Tip:** In CI and containers, prefer a **service token** granted only the environments it needs. It requires no passphrase and can be revoked independently. See [Service tokens](/docs/guides/service-tokens). ## Building the CLI From the monorepo, the CLI builds to a self-contained bundle: ```bash pnpm --filter @seekrit/cli build node apps/cli/dist/index.js --help ``` --- # The `seekrit-run` launcher `seekrit-run` is a compiled, single-file version of `seekrit run` built for machines. It authenticates with a service token, fetches the bound environment's encrypted secrets, **decrypts them locally**, layers them under any `.env` files and the live process environment, and then `exec`s your command. That's all it does — no config files, no caching, no daemon. ```bash SEEKRIT_TOKEN=skt_… seekrit-run -- ./start-server ``` > **Note:** Use the Node `seekrit` CLI for human workflows (login, key setup, managing secrets). `seekrit-run` is **service-token only** and read-only at runtime: the smallest thing that can turn a token into an environment and hand off to your process. ## Degrades gracefully Fetching seekrit secrets is **best-effort**. If the token is missing or malformed, or the API can't be reached, `seekrit-run` logs a warning to stderr and still runs your command with just the `.env` overlay and the live environment: ``` seekrit-run: continuing without seekrit-managed secrets: could not reach the seekrit API: … ``` This means the same launcher works whether or not seekrit is reachable — locally without a token, in CI, or in an offline sandbox — so you can wire it into an image's entrypoint unconditionally. Only genuinely local problems stop it: a usage error, an explicit `--env-file` that can't be read, or a command that can't be started. The `seekrit run` subcommand of the Node CLI behaves the same way. ## Why use it instead of the CLI? - **Tiny & static.** One ~1–2 MB file with no runtime dependencies — no Node, no OpenSSL, no system CA bundle. TLS roots are compiled in, so it runs unchanged in `distroless`, `alpine`, and `scratch` images. - **Same behavior as `seekrit run`.** Identical layer precedence and `.env` parsing; its decryption is verified bit-for-bit against the browser/CLI crypto. - **Clean process model.** On Unix it replaces itself with your command (`execvp`), so signals and exit codes pass through exactly. On Windows it spawns, waits, and forwards the exit code. ## Install The install script detects your OS and architecture, downloads the matching binary from `run.seekrit.dev`, verifies its SHA-256 checksum, and drops it on your `PATH`: ```bash curl -fsSL https://run.seekrit.dev/install.sh | sh ``` It installs to `/usr/local/bin` if that's writable, otherwise `~/.local/bin`. Override the destination, pin a version, or force a target with environment variables: ```bash # Pin a version and install somewhere specific. curl -fsSL https://run.seekrit.dev/install.sh \ | SEEKRIT_RUN_VERSION=0.2.0 SEEKRIT_RUN_INSTALL_DIR="$HOME/bin" sh ``` | Variable | Default | Description | | --- | --- | --- | | `SEEKRIT_RUN_VERSION` | `latest` | Version to install, e.g. `0.2.0`. | | `SEEKRIT_RUN_INSTALL_DIR` | `/usr/local/bin` or `~/.local/bin` | Where to put the binary. | | `SEEKRIT_RUN_TARGET` | auto-detected | Force a Rust target triple. | ### Manual download Prefer to fetch it yourself? Every release is published under a versioned and a `latest/` path, each with a `.sha256` alongside. On Linux the fully static **musl** build runs anywhere (glibc, `alpine`, `distroless`, `scratch`): ```bash # Pick your target: {x86_64,aarch64}-{unknown-linux-musl,unknown-linux-gnu,apple-darwin} curl -fsSL -O https://run.seekrit.dev/latest/seekrit-run-aarch64-apple-darwin.tar.gz curl -fsSL -O https://run.seekrit.dev/latest/seekrit-run-aarch64-apple-darwin.sha256 shasum -a 256 -c seekrit-run-aarch64-apple-darwin.sha256 tar xzf seekrit-run-aarch64-apple-darwin.tar.gz install -m 0755 seekrit-run /usr/local/bin/ ``` Windows ships as a `.zip` (`seekrit-run-x86_64-pc-windows-msvc.zip`). ## Usage ```bash # Token from the environment (or a .env file); resolves org/app/env from it. SEEKRIT_TOKEN=skt_… seekrit-run -- pnpm start # Token via flag; everything after -- is the command, verbatim. seekrit-run --token skt_… -- node --enable-source-maps server.js # Point at a self-hosted API. seekrit-run --api-url https://api.your-seekrit.example -- ./app # Swap one composed group's slice for this run. seekrit-run --with auth-providers=staging -- ./app # See where each variable resolved from (stderr; names only, never values). seekrit-run --explain -- true ``` ## Precedence Highest wins — identical to `seekrit run`: ``` process env > .env files > app-environment secrets > group secrets ``` The live process environment always wins, so a value exported in the shell (or by your orchestrator) overrides anything seekrit resolves. ## Options | Flag | Default | Description | | --- | --- | --- | | `-t, --token ` | `SEEKRIT_TOKEN` (env or `.env`) | Service token. | | `--api-url ` | `SEEKRIT_API_URL` or `https://api.seekrit.dev` | API base URL. | | `-e, --env-file ` | `.env` | A `.env` file to overlay (repeatable). | | `--no-env-file` | | Do not load the default `.env`. | | `--with ` | | Override one composed group's slice (repeatable). | | `--explain` | | Print each variable's source to stderr. | `HTTPS_PROXY` / `ALL_PROXY` are honored for egress-proxied networks. ## In a container Keep Node out of your runtime image entirely — fetch the static binary in a build stage (pin the version and verify the checksum for reproducible images), then copy it into a minimal runtime and make it the entrypoint: ```dockerfile # --- fetch seekrit-run: pinned + checksum-verified -------------------------- FROM alpine:3 AS seekrit ARG SEEKRIT_RUN_VERSION=0.2.0 ARG TARGET=x86_64-unknown-linux-musl RUN apk add --no-cache curl && cd /tmp \ && curl -fsSL -O https://run.seekrit.dev/v${SEEKRIT_RUN_VERSION}/seekrit-run-${TARGET}.tar.gz \ && curl -fsSL -O https://run.seekrit.dev/v${SEEKRIT_RUN_VERSION}/seekrit-run-${TARGET}.sha256 \ && sha256sum -c seekrit-run-${TARGET}.sha256 \ && tar xzf seekrit-run-${TARGET}.tar.gz -C /usr/local/bin # --- your runtime image: fully static, so distroless/static is enough ------- FROM gcr.io/distroless/static COPY --from=seekrit /usr/local/bin/seekrit-run /usr/local/bin/seekrit-run ENTRYPOINT ["seekrit-run", "--"] CMD ["./start-server"] ``` ```bash docker run --rm \ -e SEEKRIT_TOKEN="$SEEKRIT_TOKEN" \ your-image ./start-server ``` > **Warning:** Inject at runtime, never at build time. Secrets fetched during `docker build` can be baked into an image layer. `seekrit-run` only ever puts values into the child process's environment — nothing touches disk. ## Exit codes | Code | Meaning | | --- | --- | | `2` | Usage error (unknown flag, no command). | | `1` | An explicitly named `--env-file` could not be read. | | `127` | The command could not be started. | | _child_ | Otherwise, the command's own exit code. | A missing/malformed token, a revoked token or missing key grant, and network failures are **not** fatal — `seekrit-run` warns and runs the command without the managed secrets (see [Degrades gracefully](#degrades-gracefully)). See the [CLI reference](/docs/reference/cli#seekrit-run-launcher) for the condensed version, and [Service tokens](/docs/guides/service-tokens) for how to mint the token this binary needs. --- # Environments & groups Most apps share the bulk of their configuration. seekrit models this with **groups**: reusable secret bags that application environments compose. An application environment is then just *its own secrets + the groups it pulls in*, resolved and layered at runtime. ## The model - A **group** is an org-scoped secret bag with its own environments, keyed by slug (`dev`, `staging`, `production`, `sandbox`, …). - An **application environment** composes one or more groups. At resolve time each group is matched to the environment whose slug matches the app environment's. - Values **layer**, lowest precedence first: ``` group secrets < app-env secrets < .env file < process env (highest wins) ``` So a shared `DATABASE_URL` in a group is overridden by the app's own value, which is overridden by a local `.env`, which is overridden by an exported shell variable. ## Setting it up Say an `api` and a `worker` share ~80% of their config. Put the shared values in a group and compose it into each app's environments. ```bash # 1. A shared group with a per-environment value set seekrit group create --name "Common backend" --slug common seekrit group env create --group common --name Production --slug production seekrit secrets set DATABASE_URL 'postgres://…' --group common --env production # 2. Compose it into each app environment seekrit env groups add --app api --env production --group common seekrit env groups add --app worker --env production --group common # 3. App-specific secrets live on the app environment seekrit secrets set QUEUE_NAME jobs --app worker --env production ``` Now `worker/production` resolves to `common@production` overlaid with `worker/production`'s own keys. Composing more than one group? Precedence follows `--position` (higher wins); pass it on `env groups add`. ## Local `.env` and process env `seekrit run` auto-loads `.env` from the working directory and overlays it above the managed layers, and the live shell overrides everything. This makes seekrit the single way your app gets its environment while still letting you tweak individual values locally. ```bash echo 'VITE_CLIENT_BASE_URL=http://localhost:5173' > .env SEEKRIT_TOKEN=skt_… seekrit run -- pnpm dev # .env wins over managed secrets ``` Point at other files with `--env-file` (repeatable; later files win), and see exactly where each value came from with `--explain`: ```bash seekrit run --env-file .env --env-file .env.local --explain -- pnpm dev ``` ## Swapping one group per boot Group environments double as **variants**. To boot with a different slice of a single group — say staging auth keys — while keeping everything else at `dev`, override just that group: ```bash seekrit run --with auth-providers=staging -- pnpm dev ``` Everything else resolves at the app environment's slug; only `auth-providers` switches to its `staging` slice. Overrides are fail-closed: you can only pull a slice you hold a key grant for. As a logged-in developer you hold them all; for a service token, pre-authorize alternate slices at creation with `token create … --allow auth-providers=staging`. > **Note:** Committing a config file no longer pins an environment. `seekrit.json` names only org + app; the environment is selected by the service token (or `--env`). --- # Service tokens Service tokens are credentials for machines — CI jobs, Docker builds, Kubernetes workloads, and agent sandboxes. Each is an independent principal **bound to one application environment**: that binding selects the org, app, and environment it resolves at runtime, plus the group slices composed into it. A token can be revoked on its own. ## How they work A service token is self-contained: the token string carries its own private key. The server stores only a **SHA-256 hash** of the token (to authenticate it) and its **public key** (to wrap environment keys to it). This means a machine holding the token can unwrap any environment DEK granted to it entirely offline — the server never had the token's private key. Because tokens are generated on the client, the full token is shown **once** at creation. Copy it then; it cannot be retrieved later. ## Creating a token **In the web dashboard:** Organization → **Service tokens** → **Mint token**. Copy the `skt_…` value from the one-time dialog. **With the CLI**, bind the token to an application environment. Creation auto-grants that environment's key **and** every group composed into it (at the matching slug): ```bash seekrit token create --name ci-deploy --app storefront --env production # prints: skt_XXXXXXXX_... (save it now) ``` To also authorize an alternate group slice for `run --with` overrides, add `--allow`: ```bash seekrit token create --name dev --app storefront --env dev --allow auth-providers=staging ``` Pass `--no-grant` to mint the token without any grants, then grant environments explicitly: ```bash seekrit token create --name ci-deploy --app storefront --env production --no-grant seekrit grant --token skt_XXXXXXXX --app storefront --env production ``` ## Admin tokens By default a token holds **member**-level API access — its real power is the environment keys wrapped to it. Pass `--admin` to mint an **org-scoped admin token** that also passes admin-gated routes: creating apps, groups, and environments, composing groups, granting keys, and minting further tokens. This is what lets automation and AI agents provision structure headlessly, with no browser session. ```bash seekrit token create --name agent-admin --admin # org-scoped: no --app/--env needed ``` An admin token needs no environment binding to provision, but you can still bind one (`--admin --app storefront --env production`) so it can both manage structure and decrypt that environment. Only an admin caller (an admin user, or another admin token) may create an admin token — capability never escalates itself. Scope admin tokens tightly and prefer a short lifetime. ## Using a token Set it as `SEEKRIT_TOKEN`; it selects the org, app, and environment on its own: ```bash export SEEKRIT_TOKEN=skt_XXXXXXXX_... export SEEKRIT_API_URL=https://api.your-seekrit.example seekrit export --format dotenv seekrit run -- ./deploy.sh ``` Service tokens never need a passphrase — their private key is in the token string. A token can only read or write the environments it was granted (its bound env + composed groups); it cannot reach other environments in the org. ## Granting and revoking Grants are per-environment. Grant a token from any environment's **Key access** panel, or with `seekrit grant --token --app --env `. When a token is retired: ```bash seekrit token revoke skt_XXXXXXXX ``` Revocation stops the token from authenticating immediately. As with any principal, revoking the grant doesn't rotate the key — see [Access & key grants](/docs/concepts/access-control#revocation-vs-rotation). > **Warning:** Treat a service token like a password with decryption power. Scope it to only the environments it needs, store it in your platform's secret store (not in the repo), and rotate the environment key if a token leaks. ## Listing tokens ```bash seekrit token list ``` Shows each token's id, name, status (active / expired / revoked), and last-used time. --- # AI agents seekrit speaks [MCP](https://modelcontextprotocol.io) through `seekrit mcp`, a stdio server bundled in the CLI. Point an agent (Claude Code, or any MCP client) at it and the agent can create orgs, apps, groups, and environments, manage secrets, mint and grant service tokens, and run commands with secrets injected — all as tools. ## Why the server is local seekrit is zero-knowledge: secret values, data keys, and private keys never reach the server. Decryption only ever happens where the credential lives. So the MCP server runs **on your machine**, next to that credential — a hosted `mcp.seekrit.dev` could only serve metadata, never decrypt. Everything that produces plaintext (a secret value, a data key, a decryption-capable grant) stays on the client. That is the whole point of seekrit, preserved. ## Setup Register the server with your agent, passing a credential in its environment: ```bash # Claude Code — an admin token lets the agent provision structure too: SEEKRIT_TOKEN=skt_… SEEKRIT_API_URL=https://api.your-seekrit.example \ claude mcp add seekrit -- seekrit mcp ``` Any MCP client works — configure a stdio server whose command is `seekrit mcp` with `SEEKRIT_TOKEN` (and `SEEKRIT_API_URL`) in its environment. ## Choosing the credential The server authenticates exactly like the CLI — `SEEKRIT_TOKEN`, `SEEKRIT_DEV_USER`, or saved config. Pick per what the agent needs to do: | Credential | Good for | Notes | | --- | --- | --- | | **Admin token** (`--admin`) | Provisioning: create apps/groups/envs, compose, grant, mint tokens | Org-scoped; the only headless way to create structure. | | **Runtime token** (bound to an env) | Reading/writing/injecting one environment's secrets | Self-decrypts — no passphrase. Cannot provision. | | **User + `SEEKRIT_PASSPHRASE`** | A human handing their own access to a local agent | Needed to create an *org* (tokens can't own a Stytch org). Set the passphrase in the server env — stdin is the transport, so there is no prompt. | Mint an admin token for an agent session, then let the agent mint scoped runtime tokens for the workloads it sets up: ```bash seekrit token create --name agent-session --admin ``` ## Tools > **Note:** Tool names may be prefixed by your client (e.g. `seekrit:create_app`). - **Discover** — `whoami`, `list_orgs`, `list_apps`, `list_envs`, `list_groups`, `list_group_envs`, `list_env_groups`, `list_members`, `list_secrets`, `list_tokens`, `audit`. - **Provision** — `create_org`, `create_app`, `create_group`, `create_env`, `create_group_env`, `compose_group`, `uncompose_group`. - **Secrets** — `set_secret`, `get_secret`, `delete_secret`. - **Tokens & access** — `create_token`, `revoke_token`, `grant_env`. - **Use & wire up** — `run_command`, `export_env`, `configure_project`. ## Use secrets without exposing them Prefer **`run_command`**: it resolves the environment, injects the secrets into a child process, and returns only the command's exit code and output — the secret values never enter the agent's context. `get_secret` returns metadata by default; it only decrypts the plaintext into the response when you pass `reveal: true`. Reach for that only when the value itself is the thing you need. > **Warning:** Revealing a secret puts its plaintext in the agent's conversation, where it may be logged or retained. Use `run_command` (or `export_env` to a gitignored file) whenever the agent needs to *use* a secret rather than *read* it. ## A typical session An agent, holding an admin token, standing up a new service: 1. `create_app` → `create_env` (production) — the data key is generated locally. 2. `set_secret` for `DATABASE_URL`, `API_KEY`, … (encrypted on this machine). 3. `create_token` bound to that env — a runtime token, auto-granted its keys. 4. `configure_project` to write `seekrit.json`, then hand the runtime token to CI or a container to run `seekrit run` / `seekrit-run`. 5. `run_command -- pnpm test` to verify the app boots with its secrets injected. ## Limits worth knowing - **Creating an organization needs a user session.** A Stytch org must have a human owner, so `create_org` works under user auth (or local `AUTH_MODE=dev`), not under a token. Everything *inside* an org is available to an admin token. - **Capability doesn't escalate.** A runtime (member) token is denied every admin route; granting a token decryption requires the caller to already hold that environment's key, so access only propagates from an existing holder. --- # CI/CD & containers The pattern for every automated environment is the same: provide a **service token**, point the CLI at your API, and either `run` your command with secrets injected or `export` them. No passphrase is involved. Store the token in your platform's secret store as `SEEKRIT_TOKEN`. The token is bound to one application environment, so `seekrit run`/`export` need no `--app`/`--env` flags — it resolves its own org, app, environment, and composed groups. ## GitHub Actions ```yaml jobs: deploy: runs-on: ubuntu-latest env: SEEKRIT_TOKEN: ${{ secrets.SEEKRIT_TOKEN }} SEEKRIT_API_URL: https://api.your-seekrit.example steps: - uses: actions/checkout@v4 - run: npm install -g @seekrit/cli - run: seekrit run -- ./deploy.sh # secrets injected into the process ``` ## Docker Prefer injecting secrets **at runtime**, not baking them into an image. In containers the best fit is [`seekrit-run`](/docs/guides/run) — a tiny static binary with no Node/OpenSSL/CA dependency, so it works in `distroless`, `alpine`, and `scratch` images. Copy it in and make it the entrypoint: ```dockerfile # Fetch the static musl build in a build stage (see the run guide for the full # pinned + checksum-verified version), then copy it into your runtime image. COPY --from=seekrit /usr/local/bin/seekrit-run /usr/local/bin/seekrit-run ENTRYPOINT ["seekrit-run", "--"] CMD ["./start-server"] ``` The [`seekrit-run` guide](/docs/guides/run#in-a-container) has a complete multi-stage Dockerfile that downloads the binary from `run.seekrit.dev` and verifies its checksum. ```bash docker run --rm \ -e SEEKRIT_TOKEN="$SEEKRIT_TOKEN" \ -e SEEKRIT_API_URL="https://api.your-seekrit.example" \ your-image ./start-server ``` If Node is already in your image you can use the CLI instead — same behavior: ```dockerfile # entrypoint.sh #!/bin/sh exec seekrit run -- "$@" ``` If you must materialize a file (for tools that read `.env`), write it inside the container at startup and keep it out of any image layer: ```bash seekrit export --format dotenv > /run/secrets.env ``` > **Warning:** Never `seekrit export` into a build stage that gets committed to an image layer. Fetch secrets at container start, into a tmpfs or process environment. ## Kubernetes Store the token in a Kubernetes Secret and reference it as an environment variable; run your app through the CLI: ```yaml env: - name: SEEKRIT_TOKEN valueFrom: secretKeyRef: name: seekrit-token key: token - name: SEEKRIT_API_URL value: https://api.your-seekrit.example command: ["seekrit", "run", "--"] args: ["./start-server"] ``` ## AI agent sandboxes Ephemeral environments — like the throwaway sandboxes an AI coding agent spins up — are a natural fit: create a short-lived environment, grant a scoped token, and let the agent's process read secrets through the CLI without ever seeing long-lived credentials. Revoke the token when the sandbox is torn down. > **Note:** A dedicated agent-proxy that swaps tokenized placeholders for real credentials on outbound requests (so an agent never sees secret values at all) is on the roadmap. The grant/wrap model already supports it — a proxy is just another principal. --- # CLI commands `seekrit [options]`. At runtime a **service token selects the org, app, and environment** — so `seekrit run`/`export` need no config file. Management commands select their target with `--org`/`--app`/`--group`/`--env` flags, falling back to the optional `seekrit.json` written by `seekrit init`. ## Environment variables | Variable | Purpose | | --- | --- | | `SEEKRIT_TOKEN` | Service token (`skt_…`). Carries its bound org + app + environment. | | `SEEKRIT_DEV_USER` | Dev identity email (local API with `AUTH_MODE=dev`). | | `SEEKRIT_API_URL` | API base URL (overrides saved config). | | `SEEKRIT_PASSPHRASE` | Passphrase to unlock your private key non-interactively. | Precedence: environment variables override values saved by `seekrit login` in `~/.config/seekrit/config.json`. ## Auth & identity ### `seekrit login` Persist credentials to the config file. | Flag | Description | | --- | --- | | `--token ` | Service token (`skt_…`). | | `--dev-user ` | Dev identity (local `AUTH_MODE=dev`). | | `--api-url ` | API base URL. | ### `seekrit whoami` Show the authenticated identity. For a service token, prints its bound `org/app/env` scope. ### `seekrit keys setup` Generate your P-256 keypair and upload your public key plus a passphrase-encrypted private key. Run once per account. Honors `SEEKRIT_PASSPHRASE`, otherwise prompts. ## Resources ### `seekrit init` `--org --app ` — write `seekrit.json` naming default org + app for management commands. **Environment-independent and safe to commit** — it never pins an environment (the token does that at runtime). ### `seekrit org create` `--name --slug ` — create an organization (you become owner). ### `seekrit app create` `[--org ] --name --slug ` — create an application in an org. ### `seekrit env create` `[--org ] --app --name --slug ` — create an application environment. Generates the environment's data key locally and wraps it to your public key. ### `seekrit group create` `[--org ] --name --slug ` — create a **group**: a reusable secret bag shared across applications. ### `seekrit group env create` `[--org ] --group --name --slug ` — create a group environment (a per-slug value set / variant). Generates its data key locally. ## Composition (`env groups`) Compose shared groups into an application environment. At resolve time each group is matched to the environment whose slug matches the app environment's (or a `--with` override). | Command | Description | | --- | --- | | `seekrit env groups add --app --env --group [--position ]` | Compose a group (higher position wins). | | `seekrit env groups list --app --env ` | List composed groups and their precedence. | | `seekrit env groups rm --app --env --group ` | Remove a group. | ## Secrets Every `secrets` command targets one environment via flags: an application environment (`--app --env`, or the config's app + `--env`) or a group environment (`--group --env`). `--org` is inferred from `seekrit.json` or a lone org. | Command | Description | | --- | --- | | `seekrit secrets list --env [--app \|--group ]` | List secret names, versions, update times (no values). | | `seekrit secrets get --env …` | Decrypt and print one value. | | `seekrit secrets set [value] --env …` | Encrypt and store a value. Reads stdin if `value` is omitted or `-`. | | `seekrit secrets rm --env …` | Delete a secret. | ## Running & exporting Both resolve a **layered** environment for the current principal: ``` group secrets < app-env secrets < .env file < process env (highest wins) ``` With a service token, org/app/env come from the token. As a logged-in user, pass `--app --env` (and `--org` if ambiguous). | Flag | Applies to | Description | | --- | --- | --- | | `--with =` | run, export | Resolve one group at a different slug for this invocation (repeatable). | | `--env-file ` | run, export | A `.env` file to overlay; repeatable; defaults to `.env`. | | `--explain` | run, export | Print each variable's source layer to stderr (never values). | ### `seekrit run -- ` Run a command with the resolved environment injected. Everything after `--` is the command. ```bash SEEKRIT_TOKEN=skt_… seekrit run -- ./start-server # swap only the auth group to its staging slice for this boot: seekrit run --with auth-providers=staging -- pnpm dev ``` Resolving seekrit secrets is **best-effort**: if no credentials are configured or seekrit can't be reached (network, auth, or decryption failure), `run` logs a warning to stderr and still launches the command with just the `.env` overlay and `process.env`. This mirrors the [`seekrit-run` launcher](#seekrit-run-launcher). (`seekrit export` does not degrade — it errors if it can't resolve the secrets.) ### `seekrit export` Print the resolved secrets (managed layers + `.env`, without `process.env`). `--format ` (default `dotenv`). ## `seekrit-run` launcher `seekrit-run` is a separate, compiled single-file binary — a dependency-free `seekrit run` for machines (containers, CI, agents). It is **service-token only** and reproduces `seekrit run`'s precedence and `.env` parsing exactly. See the [launcher guide](/docs/guides/run) for install and container usage. ``` seekrit-run [OPTIONS] [--] [args...] seekrit-run run [OPTIONS] [--] [args...] # `run` is optional ``` | Flag | Default | Description | | --- | --- | --- | | `-t, --token ` | `SEEKRIT_TOKEN` (env or `.env`) | Service token. | | `--api-url ` | `SEEKRIT_API_URL` or `https://api.seekrit.dev` | API base URL. | | `-e, --env-file ` | `.env` | A `.env` file to overlay (repeatable). | | `--no-env-file` | | Do not load the default `.env`. | | `--with ` | | Override one composed group's slice (repeatable). | | `--explain` | | Print each variable's source to stderr (names only). | Like `seekrit run`, it degrades gracefully: a missing/malformed token or an unreachable API is logged to stderr, and the command runs with just `.env` + the live environment. Exit codes: `2` usage error, `1` unreadable explicit `--env-file`, `127` command not found; otherwise the command's own exit code (Unix `exec`). Honors `HTTPS_PROXY` / `ALL_PROXY`. Note its default API URL is the hosted `https://api.seekrit.dev` (the Node CLI defaults to your saved config). ## Access ### `seekrit grant` Grant an environment's key to a principal. Target the environment with `--env` plus `--app` or `--group`; then exactly one of: | Flag | Description | | --- | --- | | `--user ` | Grant to an org member (must have completed key setup). | | `--token ` | Grant to a service token (`skt_…`). | ## Service tokens | Command | Description | | --- | --- | | `seekrit token create --name --app --env [--allow =] [--no-grant]` | Mint a **runtime** token bound to an app environment; prints it once. Auto-grants that env's key and every composed group's matching slice. `--allow` pre-authorizes an alternate group slice for `run --with`; `--no-grant` skips granting. | | `seekrit token create --name --admin [--org ]` | Mint an **admin** token: org-scoped, no env binding, passes admin-gated routes (create apps/groups/envs, compose, grant, mint tokens). For headless provisioning by agents/automation. Only an admin caller may create one. | | `seekrit token list [--org ]` | List tokens with role, status, and last-used time. | | `seekrit token revoke [--org ]` | Revoke a token. | An admin token can also be bound to an environment (pass `--admin --app --env`) to both provision structure **and** decrypt that environment. ## Agent integration ### `seekrit mcp` Run an [MCP](https://modelcontextprotocol.io) server over stdio so AI agents (Claude Code and other MCP clients) can drive seekrit as tools. It reads the same credentials as every other command (`SEEKRIT_TOKEN` / `SEEKRIT_DEV_USER`, or the saved config). ```bash # Register with Claude Code (token selects the org; admin token enables provisioning): SEEKRIT_TOKEN=skt_… claude mcp add seekrit -- seekrit mcp ``` All decryption happens **locally**, in this process — the server exposes the tools that touch plaintext (secret values, data keys, decryption-capable grants), which is why it runs on your machine rather than a hosted endpoint. Tools include `create_org`/`create_app`/`create_env`, `set_secret`/`get_secret`, `create_token`, `grant_env`, and `run_command` (inject secrets into a subprocess without returning their values). Because stdin is the transport, user-auth sessions that decrypt need `SEEKRIT_PASSPHRASE` set (token auth needs nothing extra). Prefer `run_command` over `get_secret` with `reveal:true` so plaintext never enters the agent's context. ## Audit ### `seekrit audit` Print the org's audit trail. `[--org ] --limit ` (default 50). --- # REST API The API is a Cloudflare Worker. All application endpoints are under `/v1` and require authentication. Values are always **ciphertext** — the API neither encrypts nor decrypts. ## Authentication Send one of: - `Authorization: Bearer ` — a Stytch B2B session JWT (web sessions). - `Authorization: Bearer skt_…` — a service token. Its stored `role` is `member` (runtime credential) or `admin` (passes admin-gated routes, for headless provisioning). `owner` is never issued to a token. - `x-seekrit-dev-user: ` — local dev only, when the worker runs with `AUTH_MODE=dev`. Errors use `{ "error": { "code": "...", "message": "..." } }` with a matching HTTP status (`400`, `401`, `403`, `404`, `409`, `500`). Organizations you don't belong to return `404`. ## Health | Method | Path | Description | | --- | --- | --- | | `GET` | `/health` | Liveness check (no auth). | ## Identity | Method | Path | Description | | --- | --- | --- | | `GET` | `/v1/me` | The current user and their organizations. | | `GET` | `/v1/me/keys` | Your public key and passphrase-encrypted private key. | | `PUT` | `/v1/me/keys` | Upload your keys (one-time key setup). | ## Organizations | Method | Path | Description | | --- | --- | --- | | `GET` | `/v1/orgs` | List organizations you can access. | | `POST` | `/v1/orgs` | Create an organization (creator becomes owner). | | `GET` | `/v1/orgs/:orgId` | Get one organization and your role. | | `GET` | `/v1/orgs/:orgId/members` | List members (includes public keys, for granting). | ## Applications & environments | Method | Path | Description | | --- | --- | --- | | `GET` | `/v1/orgs/:orgId/apps` | List applications. | | `POST` | `/v1/orgs/:orgId/apps` | Create an application. *(admin)* | | `GET` | `/v1/orgs/:orgId/apps/:appId` | Get an application and its environments. | | `DELETE` | `/v1/orgs/:orgId/apps/:appId` | Delete an application. *(admin)* | | `GET` | `/v1/orgs/:orgId/apps/:appId/envs` | List environments. | | `POST` | `/v1/orgs/:orgId/apps/:appId/envs` | Create an environment (body includes the wrapped DEK). *(admin)* | | `GET` | `/v1/orgs/:orgId/envs/:envId` | Get an environment. | | `DELETE` | `/v1/orgs/:orgId/envs/:envId` | Delete an environment. *(admin)* | An environment is owned by **either** an application or a group; `applicationId` and `groupId` are both present on the row, with the unused one `null`. ## Groups A group is a reusable, org-scoped secret bag. Its environments (matched by slug) reuse the same secret and key-grant endpoints above. | Method | Path | Description | | --- | --- | --- | | `GET` | `/v1/orgs/:orgId/groups` | List groups. | | `POST` | `/v1/orgs/:orgId/groups` | Create a group. *(admin)* | | `GET` | `/v1/orgs/:orgId/groups/:groupId` | Get a group and its environments. | | `DELETE` | `/v1/orgs/:orgId/groups/:groupId` | Delete a group. *(admin)* | | `GET` | `/v1/orgs/:orgId/groups/:groupId/envs` | List a group's environments. | | `POST` | `/v1/orgs/:orgId/groups/:groupId/envs` | Create a group environment (body includes the wrapped DEK). *(admin)* | ### Composition Which groups an application environment pulls in, and their precedence. | Method | Path | Description | | --- | --- | --- | | `GET` | `/v1/orgs/:orgId/envs/:envId/groups` | List composed groups (slug, name, position). | | `POST` | `/v1/orgs/:orgId/envs/:envId/groups` | Compose a group `{ groupId, position? }`. *(admin)* | | `DELETE` | `/v1/orgs/:orgId/envs/:envId/groups/:groupId` | Remove a composed group. *(admin)* | ## Resolve | Method | Path | Description | | --- | --- | --- | | `GET` | `/v1/resolve` | The full layered environment for the calling principal. | For a service token, the org + app + environment come from the token itself. For a user session pass `?env=`. Repeat `?with=:` to resolve specific groups at a different slug. Returns `{ scope, layers }`, where `layers` are ordered lowest-precedence first (composed groups → the app environment) and each carries its `environmentId`, secret ciphertexts, and the DEK wrapped to the caller. The client decrypts and merges; the server sees only ciphertext. > **Note:** Resolve is edge-cached per principal, so a fleet of identical containers or CI jobs sharing one service token is served from cache without re-querying the database. Responses reflect secret writes, key changes, and composition changes within seconds (a write invalidates the cache; a short TTL is the backstop). Because the cache key includes the calling principal, one caller never receives another's wrapped DEK. Successful resolves are metered for usage/billing but are not written to the audit log; a *denied* resolve is audited (`env.resolve_denied`). ## Secrets Values are opaque ciphertext blobs produced by the client. | Method | Path | Description | | --- | --- | --- | | `GET` | `/v1/orgs/:orgId/envs/:envId/secrets` | List secrets (with ciphertext). | | `PUT` | `/v1/orgs/:orgId/envs/:envId/secrets/:name` | Create or update a secret (appends a version). | | `DELETE` | `/v1/orgs/:orgId/envs/:envId/secrets/:name` | Delete a secret. | ## Key grants | Method | Path | Description | | --- | --- | --- | | `GET` | `/v1/orgs/:orgId/envs/:envId/key` | The calling principal's own wrapped DEK. | | `GET` | `/v1/orgs/:orgId/envs/:envId/keys` | List all grants for the environment. *(admin)* | | `POST` | `/v1/orgs/:orgId/envs/:envId/keys` | Grant a wrapped DEK to a principal. *(admin)* | | `DELETE` | `/v1/orgs/:orgId/envs/:envId/keys/:grantId` | Revoke a grant. *(admin)* | ## Service tokens | Method | Path | Description | | --- | --- | --- | | `GET` | `/v1/orgs/:orgId/tokens` | List service tokens. *(admin)* | | `POST` | `/v1/orgs/:orgId/tokens` | Register a token (client sends hash + public key; `role` defaults to `member`, pass `admin` for a provisioning token; optional `environmentId` binds it to an app environment). *(admin)* | | `DELETE` | `/v1/orgs/:orgId/tokens/:tokenId` | Revoke a token. *(admin)* | A service token may read or write secrets only for environments it holds a key grant for — its bound environment and the group slices composed into it. It cannot address other environments in the org. ## Audit | Method | Path | Description | | --- | --- | --- | | `GET` | `/v1/orgs/:orgId/audit` | Paginated audit entries. *(admin)* Query: `cursor`, `limit`, `action`, `resourceType`. | > **Note:** Token creation is client-driven: the client generates the keypair and token string locally and submits only the token id, a SHA-256 hash, and the public key. The server never sees the token secret.