stash
v1.1.1
Published
CipherStash CLI — the one stash command for auth, init, encryption schema, database setup, and secrets.
Readme
stash
The single CLI for CipherStash. It handles authentication, project initialization, EQL v3 installation/upgrades, validation, migration rollout, and schema building.
Quickstart
npm install -D stash
npx stash auth login # authenticate with CipherStash
npx stash init # scaffold, introspect, install EQLstash init authenticates, resolves DATABASE_URL, introspects the database, scaffolds an encryption client, installs dependencies and EQL v3, and writes .cipherstash/context.json.
The agent handoff belongs to the next two commands — stash plan drafts a reviewable .cipherstash/plan.md, and stash impl executes it. Both present the same four targets:
- Hand off to Claude Code — copies the per-integration set of skills (
stash-encryption,stash-<integration>,stash-cli) into.claude/skills/, writes.cipherstash/context.jsonandsetup-prompt.md, then launchesclaudeinteractively. - Hand off to Codex — copies the same skills into
.codex/skills/, writes a sentinel-managedAGENTS.md(durable doctrine), plus.cipherstash/context files, then launchescodex. - Use the CipherStash Agent — runs the in-house wizard (
@cipherstash/wizard). - Write AGENTS.md — for editor agents (Cursor / Windsurf / Cline) that don't auto-load skill directories. Writes a single
AGENTS.mdwith the doctrine plus the relevant skill content inlined under a sentinel block, and stops.
Pass --target <claude-code|codex|agents-md|wizard> to skip the picker. It is required when running plan or impl non-interactively (CI, pipes, an agent's shell) — the picker reads from /dev/tty, so without it the command prints a hint and exits without handing off.
A project-specific action plan is written to .cipherstash/setup-prompt.md regardless of which target you pick — it tells the agent exactly what's already done and what's left, with the right commands for your package manager and ORM. The matching context (selected columns, env keys, paths, versions) is at .cipherstash/context.json.
If neither claude nor codex is on PATH, the handoff still writes the rules files and prints install instructions — your progress is never wasted.
Recommended flow
npx stash auth login
└── npx stash init ← introspects DB, installs EQL, writes context.json
└── npx stash plan ← drafts .cipherstash/plan.md for review
└── npx stash impl ← agent edits schema files / generates migrations
└── npx stash status ← where am I?stash covers authentication, initialization, EQL install/upgrade/status, schema introspection, and the staged EQL v3 encryption rollout.
Configuration
stash.config.ts is the single source of truth for database-touching commands. Create it in your project root:
import { defineConfig } from 'stash'
export default defineConfig({
databaseUrl: process.env.DATABASE_URL!,
client: './src/encryption/index.ts',
})| Option | Required | Default | Description |
|--------|----------|---------|-------------|
| databaseUrl | Yes | — | PostgreSQL connection string |
| client | No | ./src/encryption/index.ts | Path to your encryption client file |
The CLI loads .env files automatically before reading the config, so process.env references work without extra setup. The config file is resolved by walking up from the current working directory.
Commands that consume stash.config.ts: eql install, eql upgrade, eql validate, eql status, db test-connection, schema build, and encrypt *.
Commands reference
npx stash init
Set up CipherStash end-to-end: authenticate, introspect your database, install dependencies, install EQL, and hand off the rest to your local coding agent.
npx stash init [--supabase] [--drizzle] [--region <slug>]| Flag | Description |
|------|-------------|
| --supabase | Use the Supabase-specific setup flow |
| --drizzle | Use the Drizzle-specific setup flow |
| --region <slug> | Region to authenticate against (e.g. us-east-1). Skips the interactive region picker. Also settable via STASH_REGION. |
What init does, in order:
- Authenticate — re-uses an existing token if found, otherwise opens the browser device-code flow.
- Resolve
DATABASE_URL— flag → env →supabase status→ interactive prompt → hard-fail. The same resolvereql installuses. - Generate the encryption client placeholder — detects the integration and writes
./src/encryption/index.tswithout selecting columns or replacing the project's authoritative schema files. The subsequentstash plan/stash implworkflow edits the real schema. - Install dependencies —
@cipherstash/stack(runtime) andstash(dev), with a confirmation prompt. - Install EQL — runs
stash eql installagainst the resolved URL after a y/N confirm. - Checkpoint — writes
.cipherstash/context.jsonand exits. Continue withstash plan, thenstash impl, as described in Quickstart.
The full pipeline state — integration, columns, env-key names, paths, versions — is captured in .cipherstash/context.json. The action plan at .cipherstash/setup-prompt.md records what's already done and what stash plan / stash impl should do next.
CIPHERSTASH_WIZARD_URL overrides the gateway endpoint for the rulebook fetch. Useful for local-dev against a wizard gateway running on localhost.
Running init non-interactively (CI, agents, pipes): every prompt has an escape hatch, so init never blocks waiting on a TTY. Provide the region up front (--region / STASH_REGION) if you aren't already logged in and set DATABASE_URL. Init exits at a clean checkpoint and points you at stash plan --target …; run stash impl after planning. When a required value is missing in a non-TTY context the command exits non-zero with an actionable message rather than hanging.
STASH_REGION=us-east-1 DATABASE_URL=postgres://… npx stash initnpx stash auth login
Authenticate with CipherStash using a browser-based device code flow.
npx stash auth login [--region <slug>] [--json] [--no-open]| Flag | Description |
|------|-------------|
| --region <slug> | Region to authenticate against (e.g. us-east-1). Skips the interactive region picker. Also settable via STASH_REGION. |
| --json | Emit newline-delimited JSON events instead of prose (see below). Implies non-interactive — never renders the region picker, and never auto-opens a browser (the human opens the URL you hand them). |
| --no-open | Don't auto-open the verification URL in a browser (already implied by --json). |
| --supabase / --drizzle | Track the integration as the referrer. |
Saves the token to ~/.cipherstash/auth.json. Database-touching commands check for this file before running.
Triggering auth from an agent (device-code flow)
The device-code flow is designed so an agent can trigger authentication but only a human completes it in the browser. Run auth login --json in the background and read the first line — authorization_required carries the verification URL to hand to the user:
npx stash auth login --region us-east-1 --json// stdout is newline-delimited JSON, one event per line:
{"status":"authorization_required","userCode":"ABCD-1234","verificationUri":"https://…/activate","verificationUriComplete":"https://…/activate?user_code=ABCD-1234","expiresIn":899}
// … the process then blocks polling until the human authorizes in the browser …
{"status":"authorized","expiresAt":1751990400,"expiresAtIso":"2025-07-08T12:00:00.000Z"}
{"status":"device_bound"}Errors are emitted as {"status":"error","code":"…","message":"…"} and exit non-zero. In a non-TTY context without --region/STASH_REGION the command exits immediately with code: "region_required" instead of hanging on the picker.
npx stash auth regions
List the regions you can authenticate against — a first-contact affordance so you (or an agent) can discover valid --region / STASH_REGION values up front instead of learning them from an error.
npx stash auth regions # human-readable list
npx stash auth regions --json # machine-readable [{ "slug": "…", "label": "…" }]// --json output:
[{"slug":"us-east-1","label":"us-east-1 (Virginia, USA)"}, {"slug":"us-east-2","label":"us-east-2 (Ohio, USA)"}, …]The region list is currently maintained in the CLI. The intended long-term source of truth is the CipherStash region API (tracked by a
TODOinsrc/commands/auth/region.ts); when that lands, this command and the runtime SDK should both read from it.
npx stash wizard
Launch the CipherStash AI wizard. Thin wrapper around @cipherstash/wizard — the wizard ships as a separate npm package so the agent SDK stays out of the stash bundle, but you don't need to remember a second tool name.
npx stash wizard [...flags]Any flags after wizard are forwarded verbatim to the wizard package. On the first run the package manager downloads the wizard (~5s); subsequent runs are instant.
npx stash eql install
Configure your database and install CipherStash EQL extensions in a single command. Run this after npx stash init. (npx stash db install is a deprecated alias — it still works but prints a warning.)
When stash.config.ts is missing, the command offers to scaffold it. Installation is direct and EQL v3 only, using the bundle pinned by @cipherstash/eql.
npx stash eql install [options]| Flag | Description |
|------|-------------|
| --force | Reinstall even if EQL is already installed |
| --dry-run | Show what would happen without making changes |
| --supabase | Supabase-compatible install with grants for built-in roles |
| --database-url <url> | One-shot target; leaves project files untouched |
--supabase grants the built-in roles access to both eql_v3 and eql_v3_internal. Removed v2 options fail explicitly; --eql-version 2 points dump-recovery users to the upstream EQL 2.3.1 SQL release.
Good to know: The pinned EQL v3 bundle self-adapts when the install role cannot create its optional ORE operator family. In that case it disables the
*OrdOredomains; use the ordinary*Orddomains for ordering.
npx stash eql upgrade
Upgrade an existing EQL v3 installation to the package-pinned version.
npx stash eql upgrade [options]| Flag | Description |
|------|-------------|
| --dry-run | Show what would happen without making changes |
| --supabase | Use Supabase-compatible upgrade |
The install SQL is idempotent and safe to re-run. If EQL is not installed, the command suggests running npx stash eql install instead.
npx stash eql validate
Validate your encryption schema against the EQL v3 domain vocabulary, and — when a database is reachable — against what that database actually has.
npx stash eql validate [--supabase] [--database-url <url>]Schema checks (no database needed):
| Rule | Severity |
|------|----------|
| An _ord_ore domain, whose ORE operator class only a superuser can create | Warning |
| Storage-only column — encrypts and decrypts, carries no query terms | Info |
| Searchable boolean column | Error |
| Free-text match on a non-text domain | Error |
| Encrypted-JSONB search without types.Json | Error |
Database checks (skipped with a notice when no database is reachable):
| Rule | Severity |
|------|----------|
| EQL v3 is not installed (reported once; the other database checks are skipped) | Error |
| Declared column or table absent from the database | Error |
| The database column's domain has drifted from the declaration | Error |
| The column is still plain (no EQL domain) | Error |
| An _ord_ore domain on a database whose EQL install could not create the ORE operator class | Error |
| Queryable column with no functional index over its term extractor | Info |
| A declared table name that resolved in the searched schema also exists in another one | Info |
The command exits with code 1 on errors (not on warnings or info).
stash db validate still works as a deprecated alias.
npx stash db migrate
Run pending encrypt config migrations.
npx stash db migrateGood to know: This command is not yet implemented.
npx stash eql status
Show the current state of EQL in your database.
npx stash eql statusReports EQL installation status and version, database permission status, and read-only diagnostics for legacy EQL v2/Proxy configuration state.
npx stash db test-connection
Verify that the database URL in your config is valid and the database is reachable.
npx stash db test-connectionReports the database name, connected role, and PostgreSQL server version.
npx stash schema build
Build an encryption client file from your database schema using DB introspection.
npx stash schema build [--supabase]Connects to your database, lets you select tables and columns to encrypt, asks about searchable indexes, and generates a typed encryption client file.
Reads databaseUrl from stash.config.ts.
Migration mode
Use eql migration to add the EQL v3 installation to your migration history instead of applying it directly. The install then ships to every environment through the same migrate step as the rest of your schema.
Drizzle
npx stash eql migration --drizzle
npx drizzle-kit migrateHow it works:
- Runs
npx drizzle-kit generate --custom --name=<name>to create an empty migration. - Loads the pinned EQL v3 SQL.
- Writes the EQL SQL into the generated migration file.
With a custom name or output directory:
npx stash eql migration --drizzle --name setup-eql --out ./migrations
npx drizzle-kit migratedrizzle-kit must be installed in your project (npm install -D drizzle-kit). The --out directory must match your drizzle.config.ts.
Add --supabase on a Supabase-hosted Drizzle project to append the anon / authenticated / service_role grants.
Supabase
npx stash eql migration --supabase
supabase db reset # local
supabase db push # remote/linked projectThis writes supabase/migrations/<timestamp>_cipherstash_eql.sql containing the EQL v3 bundle, the Supabase role grants, and the cipherstash.cs_migrations tracking schema — so one reset provisions everything stash encrypt needs.
Use this rather than eql install --supabase whenever the project has a local supabase/ directory. A direct install does not survive supabase db reset, which drops the database and replays the migrations directory.
The file is timestamped at generation time, so it sorts after everything already applied and pushes with no extra flag. An out-of-order version is not merely skipped — supabase db push aborts the whole push with Found local migration files to be inserted before the last migration on remote database. and applies nothing until you re-run with --include-all.
If the project already has migrations that reference EQL (an eql_v3_* column added back when eql install was applied directly), those now sort before the install. supabase db reset replays in version order with no dependency awareness, so they run first and the reset fails with type "eql_v3_text_search" does not exist. The command warns and names them; rename the install migration to a version below the earliest of them so it replays first.
How that back-dated version reaches a remote depends on what that remote actually has, so check before touching the ledger:
psql "$REMOTE_DATABASE_URL" -Atc "select eql_v3.version()"eql_v3.version() is created by the bundle's last statements, so it answers "is the whole install there" — a probe for the eql_v3 schema does not, since that schema is created by the bundle's first statements and survives an install that aborted partway.
If it prints a version, EQL is present and only the ledger row is missing — mark it applied with supabase migration repair --status applied <version>, which writes the row and runs no SQL. Do not push the file there instead: that re-runs a bundle opening with DROP SCHEMA IF EXISTS eql_v3 CASCADE, dropping every index, constraint, and RLS policy that references those schemas.
If it errors, that remote genuinely still needs the SQL applied: supabase db push --include-all. Never mark it applied there — the ledger row would claim SQL that never ran, so no later push installs EQL, and the first migration referencing eql_v3 fails with nothing pointing at the cause.
Pass --force to regenerate an existing install migration in place. It keeps its version, so supabase db push will not re-apply it — pending migrations are decided by version, never by file content, and push reports Remote database is up to date. Use supabase db reset locally, or on a remote:
supabase migration repair --status reverted <version> # clear the ledger row (applies no SQL)
supabase db push # re-applyAdd --include-all to that push only if it aborts with Found local migration files to be inserted before the last migration on remote database. — that happens when migrations sort after the install, leaving the reverted version as a gap in the middle of history. Reverting the newest version leaves it at the tail, which a plain push applies. The flag applies every out-of-order migration you have, so don't pass it pre-emptively.
Weigh that before doing it to a populated database: the EQL bundle opens with DROP SCHEMA IF EXISTS eql_v3 CASCADE (and eql_v3_internal), so re-applying also drops every index, constraint, and RLS policy that references those schemas.
Don't pass --out here. The Supabase CLI reads <project>/supabase/migrations and nothing else — the path is not configurable in config.toml, and --workdir moves the whole supabase/ directory, not this one. An install written elsewhere is never applied by supabase db reset / db push, which is the failure this command exists to avoid. The flag still works (and warns) for projects that apply another directory through their own tooling.
npx stash eql repair --drizzle
Repairs migrations drizzle-kit generate emitted with an in-place ALTER COLUMN … SET DATA TYPE <eql_v3_*>, which Postgres cannot run (there is no cast from text/numeric to an EQL domain). Each is rewritten into an additive ADD COLUMN "<column>_encrypted" that preserves the source column.
npx stash eql repair --drizzle
npx drizzle-kit migrate| Flag | Description |
|------|-------------|
| --drizzle | Required. Repair a Drizzle migration directory |
| --out <path> | Directory to sweep. Default drizzle |
| --dry-run | Report what would be rewritten without writing anything |
| --database-url <url> | Leave migrations the database has already applied untouched |
This is the same sweep eql migration --drizzle performs, without generating an install migration you do not need. With --database-url it reads drizzle.__drizzle_migrations and refuses to rewrite an already-applied migration — doing so would leave the file describing a shape that database never got from it, and a fresh CI or staging database replaying it would silently diverge. Without a URL it proceeds and warns that applied state could not be verified.
Required database permissions
Before installing EQL, the CLI verifies that the connected role has:
CREATEon the database (forCREATE SCHEMAandCREATE EXTENSION).CREATEon thepublicschema (for thepublic.eql_v3_*domains).SUPERUSERor extension owner privileges (forCREATE EXTENSION pgcrypto, if not already installed).
If permissions are insufficient, the CLI exits with a message listing what is missing.
Programmatic API
import {
defineConfig,
loadStashConfig,
EQLInstaller,
loadBundledEqlSql,
} from 'stash'defineConfig
Type-safe identity function for stash.config.ts:
import { defineConfig } from 'stash'
export default defineConfig({
databaseUrl: process.env.DATABASE_URL!,
client: './src/encryption/index.ts',
})loadStashConfig
Finds and loads the nearest stash.config.ts, validates it with Zod, applies defaults, and returns the typed config:
import { loadStashConfig } from 'stash'
const config = await loadStashConfig()
// config.databaseUrl — validated non-empty string
// config.client — defaults to './src/encryption/index.ts'EQLInstaller
Programmatic access to EQL installation:
import { EQLInstaller } from 'stash'
const installer = new EQLInstaller({ databaseUrl: process.env.DATABASE_URL! })
const preflight = await installer.preflight()
if (!preflight.ok) {
console.error('Blocking gaps:', preflight.missing)
process.exit(1)
}
if (!(await installer.isInstalled())) {
const { deferredGrantsSql } = await installer.install({ supabase: true })
// Non-null when the connecting role is not a member of `postgres`: the
// optional owner-scoped default-privilege statements, skipped. The install
// is complete without them.
if (deferredGrantsSql) console.log(deferredGrantsSql)
}| Method | Returns | Description |
|--------|---------|-------------|
| preflight() | Promise<PreflightResult> | Read-only role-capability report (superuser, membership of postgres, CREATE privileges, pgcrypto placement, EQL schema presence/ownership) |
| checkPermissions() | Promise<PermissionCheckResult> | Deprecated — thin adapter over preflight(); will be removed in the next major |
| isInstalled() | Promise<boolean> | Check if the EQL v3 schemas exist |
| getInstalledVersion() | Promise<string \| null> | Get the installed EQL version |
| install(options?) | Promise<InstallResult> | Install the EQL bundle (its own transaction), then the Supabase grants (outside it, so a grants failure keeps the install) |
| applySupabaseGrants() | Promise<InstallResult> | Re-apply the Supabase role grants alone (idempotent) |
Install options: supabase.
loadBundledEqlSql
Load the bundled EQL install SQL as a string:
import { loadBundledEqlSql } from 'stash'
const sql = loadBundledEqlSql()Relationship to @cipherstash/stack
@cipherstash/stack is the runtime SDK. It stays lean with no heavy dependencies like pg and ships in your production bundle. stash is a devDependency: it handles database tooling and schema lifecycle at development time. Think of it like Drizzle Kit — a companion tool that prepares the database while the runtime SDK handles queries.
