stash
v1.0.0
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, db 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 db validate
Validate your encryption schema for common misconfigurations.
npx stash db validate [--supabase] [--exclude-operator-family]| Rule | Severity |
|------|----------|
| freeTextSearch on a non-string column | Warning |
| orderAndRange without operator families | Warning |
| No indexes on an encrypted column | Info |
| searchableJson without dataType("json") | Error |
The command exits with code 1 on errors (not on warnings or info).
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.
Drizzle migration mode
Use eql migration --drizzle to add EQL v3 installation to Drizzle migration history instead of applying it directly.
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.
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 permissions = await installer.checkPermissions()
if (!permissions.ok) {
console.error('Missing permissions:', permissions.missing)
process.exit(1)
}
if (!(await installer.isInstalled())) {
await installer.install({ supabase: true })
}| Method | Returns | Description |
|--------|---------|-------------|
| checkPermissions() | Promise<PermissionCheckResult> | Check required database permissions |
| isInstalled() | Promise<boolean> | Check if the EQL v3 schemas exist |
| getInstalledVersion() | Promise<string \| null> | Get the installed EQL version |
| install(options?) | Promise<void> | Execute the EQL install SQL in a transaction |
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.
