.env files with a cloud-based secrets manager you can self-host. Prefix your commands with sigillo run and secrets are injected as environment variables, never written to disk.12345# instead of this source .env && next dev # do this sigillo run -- next dev
┌────────────────┐ sigillo run -- next dev │ App Worker │ │ │ (sigillo.dev) │ │ 1. fetch secrets │ │ │──────────────────────────────────────▶│ decrypt │ │ { DB_URL, API_KEY, ... } │ AES-256-GCM │ │◀──────────────────────────────────────│ │ │ └────────────────┘ │ 2. spawn child with env vars │ ▼ ┌──────────────┐ │ next dev │ │ (child) │ └──────┬───────┘ │ │ 3. stdout / stderr ▼ ┌───────────────┐ │ redaction │ high-entropy values replaced with * │ filter │ secrets never reach your terminal └──────┬────────┘ │ ▼ terminal (safe output)
.env files: secrets live in the cloud and are easy to share across machines. No more "can you send me the .env?" on Slack..env files or pasting keys in DMs.-c production..env files, use sigillo run to inject secrets into processes without exposing them.sigillo run replaces secret values in stdout/stderr with *, so secrets never enter your chat context window. Even if an agent runs printenv, it won't see the real values in the output.1npx -y skills add remorses/sigillo
~/.sigillo/bin):1curl -fsSL https://sigillo.dev/install.sh | bash
1npm i -g sigillo
12npx sigillo run -- next dev bunx sigillo run -- next dev
1sigillo login
1sigillo setup
~/.sigillo/config.json (not in the repo). Run it in the project root if you have a single project, or in each subfolder of a monorepo. Since the config is local to your machine, you need to run sigillo setup again after cloning the repo on a new machine. Alternatively, skip setup entirely and always pass --project and --env (or -c) flags.1sigillo run -- next dev
.env files, no copy-pasting keys. Go back to sigillo.dev any time to add, edit, or rotate secrets. The next sigillo run picks them up automatically.Organization (my-company) │ ├── Project (api) │ ├── dev │ │ ├── DATABASE_URL = postgres://localhost/mydb │ │ ├── API_KEY = sk-dev-xxx │ │ └── AUTH_SECRET = random-dev-key │ ├── preview │ │ ├── DATABASE_URL = postgres://preview-host/mydb │ │ └── API_KEY = sk-preview-xxx │ └── prod │ ├── DATABASE_URL = postgres://prod-host/mydb │ └── API_KEY = sk-live-xxx │ └── Project (web) ├── dev │ └── NEXT_PUBLIC_API_URL = http://localhost:3001 └── prod └── NEXT_PUBLIC_API_URL = https://api.example.com
1sigillo orgs create --name my-company
12sigillo orgs # find your org ID sigillo projects create --org <ORG_ID> --name my-app
sigillo setup saves the default project and environment for the current directory. This is a local-only setting stored in ~/.sigillo/config.json, not in the repository. After setup, every sigillo run in that directory (or any subdirectory) resolves the right secrets without extra flags.1234567# single project cd my-app sigillo setup --project <PROJECT_ID> --env dev # monorepo cd monorepo/api && sigillo setup --project api_xxx --env dev cd monorepo/web && sigillo setup --project web_xxx --env dev
sigillo setup after cloning on a new machine. If you prefer not to run setup at all, you can always pass --project and --env explicitly:1sigillo run --project <PROJECT_ID> -c dev -- next dev
sigillo setup shows an interactive picker. Use --project and --env for non-interactive/CI workflows.123sigillo secrets set DATABASE_URL "postgres://localhost:5432/mydb" -c dev sigillo secrets set API_KEY "" -c dev sigillo secrets set AUTH_SECRET "" -c dev
12sigillo secrets set DATABASE_URL "" -c preview sigillo secrets set DATABASE_URL "" -c prod
123sigillo secrets set AUTH_SECRET "$(openssl rand -base64 32)" -c dev sigillo secrets set AUTH_SECRET "$(openssl rand -base64 32)" -c preview sigillo secrets set AUTH_SECRET "$(openssl rand -base64 32)" -c prod
sigillo.dev/orgs/<ORG_ID>/projects/<PROJECT_ID>/envs/dev to add or edit secrets from the web UI. You can toggle between environments using the tabs.12sigillo secrets -c dev # list secret names (values hidden) sigillo run -c dev -- pnpm dev
| Feature | Description |
| Secret injection | sigillo run -- <cmd> injects secrets as env vars, no files on disk |
| Output redaction | High-entropy values automatically replaced with * in stdout/stderr |
| File mount | --mount .env writes secrets to the given file path, deletes it after the process exits |
| Organizations | Multi-tenant orgs with admin/member roles and invite links |
| Projects & environments | Organize secrets into projects with dev/preview/production environments |
| Audit log | Append-only event log tracks every secret change with user attribution |
| API tokens | Scoped to project or single environment, SHA-256 hashed, shown once |
| Device flow | RFC 8628 login for CLI and agents, no copy-pasting tokens |
| AES-256-GCM encryption | Every secret encrypted at rest with a random 12-byte IV |
| Download formats | Export as json, env, yaml, docker, dotnet-json, xargs |
| Web UI | Full management dashboard with Doppler-style hidden values |
| Self-hostable | Runs on Cloudflare Workers + D1, deploy your own instance |
| REST API | OpenAPI-documented API for building custom integrations |
sigillo login123sigillo login # interactive device flow sigillo login --token sig_xxx # save existing API token sigillo login --api-url https://my-instance.dev --scope . # custom instance, scoped to current dir
sigillo setup~/.sigillo/config.json, not in the repo, so it needs to be done on each machine after cloning. Run it in the project root, or in each subfolder of a monorepo. You can skip setup entirely by always passing --project and --env flags to other commands.12sigillo setup # interactive project/env picker sigillo setup --project proj_abc --env dev # non-interactive
sigillo run123456789sigillo run -- next dev # inject secrets from the configured env sigillo run -c dev -- next dev # use the dev environment sigillo run -c preview -- next dev # use the preview environment sigillo run -c production -- next build # use the production environment sigillo run -- printenv # verify which vars are injected (values redacted) sigillo run --command 'echo $MY_SECRET' # shell string mode sigillo run --mount .env -- npm start # write to file, clean up after sigillo run --mount config.json --mount-format json -- next dev # mount as JSON sigillo run --disable-redaction -- ./my-script.sh # opt out of output redaction
--command when you need shell features like &&, pipes, redirects, or $VARIABLE expansion. Wrap the command in single quotes so your parent shell does not expand secret variables before Sigillo injects them.12345# Wrong: your shell expands $DATABASE_URL before sigillo starts sigillo run --command "psql $DATABASE_URL -c 'select 1'" # Right: $DATABASE_URL expands inside sigillo's child shell sigillo run --command 'psql $DATABASE_URL -c "select 1"'
sigillo run, especially in package scripts. This keeps regular build flags visible while secrets still come from Sigillo.12345{ "scripts": { "deployment": "CLOUDFLARE_ENV=preview sigillo run -c preview --command 'vite build && wrangler deploy --env preview'" } }
* in stdout/stderr. This prevents secrets from leaking into agent context windows or CI logs.sigillo run through a package manager script (pnpm run, bun run, npm run), the package manager adds node_modules/.bin to PATH before Sigillo starts. Sigillo inherits that PATH and passes it to the child process, so local binaries like vite, tsc, wrangler are all available without prefixing with pnpm exec or npx.1234567# in package.json scripts, local bins just work: sigillo run -- vite build # vite found via node_modules/.bin sigillo run -- wrangler deploy # wrangler found via node_modules/.bin sigillo run -- tsc --noEmit # tsc found via node_modules/.bin # same with --command: sigillo run --command 'vite build && wrangler deploy'
sigillo run directly with pnpm exec or bunx:12pnpm exec sigillo run -- vite dev bunx sigillo run -- next build
curl or npm i -g), running sigillo run outside a package manager script means node_modules/.bin is not in PATH. In that case, use the full path or prefix with npx/pnpm exec inside the child command, or run Sigillo from a package script instead.sigillo secrets123456789sigillo secrets # list secret names sigillo secrets get DATABASE_URL # get a single value sigillo secrets get DATABASE_URL --force # allow value output inside agent shells sigillo secrets set API_KEY sk-live-xxx # set a value echo "multiline\nvalue" | sigillo secrets set CERT # set from stdin sigillo secrets delete OLD_KEY # delete sigillo secrets download # download all (YAML) sigillo secrets download --format json # download as JSON sigillo secrets download --format env # download as .env
secrets get and secrets download refuse to print raw values to a terminal unless you pass --force. Prefer sigillo run or a direct pipe so secret values go straight to the tool that needs them, not into the chat context.12sigillo run --command 'psql "$DATABASE_URL" -c "select 1"' sigillo secrets download --format env | fly secrets import --app my-app
sigillo projects12345sigillo projects # list all projects sigillo projects create --org org_abc --name my-app # create project sigillo projects get proj_abc # show project details sigillo projects update proj_abc --name new-name # rename sigillo projects delete proj_abc # delete
sigillo environments1234sigillo environments # list environments sigillo environments create --project proj_abc --name Staging --slug staging # create sigillo environments rename env_abc --name Production --slug prod # rename sigillo environments delete env_abc # delete
| Flag | Env var | Description |
--token <sig_xxx> | SIGILLO_TOKEN | Bearer token for auth |
--api-url <url> | SIGILLO_API_URL | API endpoint (default: https://sigillo.dev) |
--env <slug> / --config <slug> / -c <slug> | SIGILLO_ENVIRONMENT | Environment slug (e.g. dev, prod) |
--project <id> / -p <id> | SIGILLO_PROJECT | Project ID override |
| Format | Flag | Use case |
json | --format json | Application config files |
env | --format env | Shell scripts with quotes |
env-no-quotes | --format env-no-quotes | Shell scripts without quotes |
yaml | --format yaml | Default CLI output |
docker | --format docker | Docker --env-file |
dotnet-json | --format dotnet-json | .NET appsettings.json (uses __ for nested keys) |
xargs | --format xargs | NUL-delimited pairs for shell pipelines |
wrangler secret bulk:1sigillo run -c production --mount .env.prod --mount-format env -- wrangler secret bulk .env.prod
package.json scripts so you can sync before each deploy:123456{ "scripts": { "secrets:preview": "sigillo run -c preview --mount .env.preview --mount-format env -- wrangler secret bulk --env preview .env.preview", "secrets:production": "sigillo run -c production --mount .env.prod --mount-format env -- wrangler secret bulk .env.prod" } }
vercel env add only accepts one variable at a time. Use the xargs format to pipe them:12sigillo secrets download -c production --format xargs | \ xargs -0 -n2 sh -c 'printf %s "$2" | vercel env add "$1" production --force' sh
--sensitive to mark values as sensitive in Vercel:12sigillo secrets download -c production --format xargs | \ xargs -0 -n2 sh -c 'printf %s "$2" | vercel env add "$1" production --sensitive --force' sh
package.json script:12345{ "scripts": { "secrets:vercel": "sigillo secrets download -c production --format xargs | xargs -0 -n2 sh -c 'printf %s \"$2\" | vercel env add \"$1\" production --sensitive --force' sh" } }
fly secrets import reads NAME=VALUE pairs from stdin. Pipe sigillo secrets download directly, no temp file needed:1sigillo secrets download -c production --format env | fly secrets import --app my-app
fly secrets import triggers a machine restart once secrets are staged. Use --stage to skip the restart and deploy separately:1234# stage without restarting sigillo secrets download -c production --format env | fly secrets import --app my-app --stage # then deploy when ready fly deploy --app my-app
package.json scripts:123456{ "scripts": { "secrets:fly:production": "sigillo secrets download -c production --format env | fly secrets import --app my-app", "secrets:fly:preview": "sigillo secrets download -c preview --format env | fly secrets import --app my-app-staging" } }
12sigillo secrets download --format docker > .env.docker docker run --env-file .env.docker my-image
1sigillo run -- docker compose up
1234567- name: Run with secrets env: SIGILLO_TOKEN: ${{ secrets.SIGILLO_TOKEN }} SIGILLO_PROJECT: ${{ vars.SIGILLO_PROJECT }} SIGILLO_ENVIRONMENT: ${{ vars.SIGILLO_ENVIRONMENT }} run: | npx sigillo run -- next build
__ become nested objects):1sigillo secrets download --format dotnet-json > appsettings.Secrets.json
DB__HOST=localhost becomes { "Db": { "Host": "localhost" } }.auth.sigillo.dev by default.Your Cloudflare account Sigillo Cloud ┌──────────────────────┐ ┌──────────────────────┐ │ App Worker │ OAuth │ Provider Worker │ │ (your secrets) │────────────▶│ (auth.sigillo.dev) │ │ │ PKCE │ │ │ You deploy this │◀────────────│ Already running │ └──────────────────────┘ └──────────────────────┘
12git clone https://github.com/remorses/sigillo.git cd sigillo && pnpm install
app/.dev.vars with your secrets:12BETTER_AUTH_SECRET=<any random string> ENCRYPTION_KEY=<output of: openssl rand -base64 32>
1pnpm --dir app dev
12pnpm --dir app deployment # deploy preview worker pnpm --dir app deployment:prod # deploy production worker
auth.sigillo.dev on first request via RFC 7591 dynamic client registration. No Google OAuth credentials needed, no manual setup.auth.sigillo.dev for authentication. If you want a fully air-gapped setup with no dependency on Sigillo cloud, you can deploy the Provider Worker yourself.provider/.dev.vars (requires Google OAuth credentials):123BETTER_AUTH_SECRET=<any random string> GOOGLE_CLIENT_ID=<your Google OAuth client ID> GOOGLE_CLIENT_SECRET=<your Google OAuth client secret>
12pnpm --dir provider deployment # deploy preview pnpm --dir provider deployment:prod # deploy production
PROVIDER_URL in app/wrangler.jsonc:12345{ "vars": { "PROVIDER_URL": "https://your-provider.your-domain.com" } }
┌─────────────────────────────────────────────────────────────────┐ │ Your Machine │ │ │ │ sigillo run -- next dev │ │ │ │ │ │ device flow login (RFC 8628) │ │ │ or bearer token │ │ ▼ │ │ ┌──────────┐ │ │ │ Sigillo │ │ │ │ CLI │ │ │ └────┬─────┘ │ │ │ │ └───────┼─────────────────────────────────────────────────────────┘ │ REST API ▼ ┌──────────────────────┐ ┌──────────────────────┐ │ App Worker │ │ Provider Worker │ │ (self-hosted) │────────▶│ (auth.sigillo.dev) │ │ │ OAuth │ │ │ • Secrets CRUD │ PKCE │ • Google login │ │ • AES-256-GCM │ │ • OAuth2 / OIDC │ │ • Audit log │◀────────│ • Dynamic client │ │ • API tokens │ token │ registration │ │ • Device flow │ │ │ │ ┌────────────┐ │ │ ┌────────────┐ │ │ │ D1 (app) │ │ │ │ D1 (auth) │ │ │ └────────────┘ │ │ └────────────┘ │ └──────────────────────┘ └──────────────────────┘
auth.sigillo.dev. Self-hosted instances register automatically via RFC 7591 dynamic client registration as public PKCE clients (no client secret needed).CLI/Agent App (self-hosted) Provider (auth.sigillo.dev) │ │ │ │ POST /api/auth/device/code │ │ │─────────────────────────────▶│ │ │ { user_code, device_code } │ │ │◀─────────────────────────────│ │ │ │ │ │ User opens /device │ │ │ and enters user_code │ │ │ ┌────────────────────┼────── redirect ───────────────▶│ │ │ │ │ │ │ │ Google sign-in ──▶│ Google │ │ │ ◀── callback ─────│ │ │ │ │ │ │ │◀── auth code (PKCE) ───────────│ │ └────────────────────┼────── approved ───────────────▶│ │ │ │ │ Poll /api/auth/device/token │ │ │─────────────────────────────▶│ │ │ { access_token } │ │ │◀─────────────────────────────│ │
Local development CI / GitHub Actions ───────────────── ─────────────────── sigillo login SIGILLO_TOKEN=sig_xxx │ │ ▼ │ Browser opens /device │ │ │ ▼ │ Enter user_code │ │ │ ▼ │ Google sign-in │ │ │ ▼ ▼ Session cookie saved Bearer token from env in ~/.sigillo/config.json var or GitHub secret │ │ ▼ ▼ sigillo run -- next dev sigillo run -- next build
sigillo login once, then the session is reused.SIGILLO_TOKEN as a secret in your CI provider. No browser needed, no interactive prompts.ENCRYPTION_KEY: 32 random bytes, base64-encoded (openssl rand -base64 32)BETTER_AUTH_SECRET via SHA-256 (default if ENCRYPTION_KEY is not set)plaintext value ("sk-live-xxx") │ ▼ ┌─────────────┐ ┌──────────────┐ │ AES-256-GCM │◀────│ 12-byte │ │ encrypt │ │ random IV │ └──────┬──────┘ └──────────────┘ │ ▼ ┌────────────────────────────────┐ │ secretEvent (append-only row) │ │ │ │ action: "set" │ │ name: "API_KEY" │ │ value: <iv>:<ciphertext> │ │ userId: usr_abc │ │ createdAt: 1719000000 │ └────────────────────────────────┘
/api/openapi.json.12345678910111213141516171819# list secrets curl -H "Authorization: Bearer sig_xxx" \ https://sigillo.dev/api/environments/{envId}/secrets # set a secret curl -X POST -H "Authorization: Bearer sig_xxx" \ -H "Content-Type: application/json" \ -d '{"name": "API_KEY", "value": "sk-live-xxx"}' \ https://sigillo.dev/api/environments/{envId}/secrets # bulk download as JSON curl -H "Authorization: Bearer sig_xxx" \ https://sigillo.dev/api/environments/{envId}/secrets/download?format=json # bulk set curl -X PUT -H "Authorization: Bearer sig_xxx" \ -H "Content-Type: application/json" \ -d '{"secrets": {"KEY1": "val1", "KEY2": "val2"}}' \ https://sigillo.dev/api/environments/{envId}/secrets