Skip to content
Security
Command

/AGENTS-doctrine

This project uses [CipherStash](https://cipherstash.com) for searchable, field-level encryption. Plaintext values are encrypted client-side via `@cipherstash/stack` before they leave the application; ciphertext is stored as `jsonb` in Postgres (or as encrypted attributes in

BOOST
From plugin
stack
1611 skill1 command
Install
$ npx -y skills add cipherstash/stack --agent claude-code

How it fires

How this command gets triggered: by you, by Claude, or both.

  • Fires itselfClaude auto-loads it when your prompt matches the work.
  • You can call itInvoke it directly when you want it.
  • Slash command/AGENTS-doctrine

Context preview

What this command does when you run it.

This project uses [CipherStash](https://cipherstash.com) for searchable, field-level encryption. Plaintext values are encrypted client-side via `@cipherstash/stack` before they leave the application; ciphertext is stored as `jsonb` in Postgres (or as encrypted attributes in

Command definition

AGENTS-doctrine.md

CipherStash

This project uses [CipherStash](https://cipherstash.com) for searchable, field-level encryption. Plaintext values are encrypted client-side via `@cipherstash/stack` before they leave the application; ciphertext is stored as `jsonb` in Postgres (or as encrypted attributes in DynamoDB) and decrypted on read.

This document is the **durable rule book** for any agent working on this codebase. Read it before touching encryption-related code. Repeatable workflows (setup, schema design, migrations) live in skills — installed as directories alongside this file, or inlined at the bottom of this file — see "Where to read more" at the bottom.

What you are working with

  • **Encryption client** — a per-project module that defines which tables and columns are encrypted, what data type each column holds, and which search operations are enabled. The path is in `.cipherstash/context.json` under `encryptionClientPath`.
  • **EQL extension** — installed via `stash eql install`, providing the column domains and server-side functions for searchable encryption. `stash eql install` installs **EQL v3** by default: the `public.eql_v3_*` column domains and the `eql_v3.*` functions behind them. (A project set up before v3 may instead carry the legacy `eql_v2` schema and `eql_v2_encrypted` column type; both generations decrypt, but new columns are v3.) Required before any migration that creates encrypted columns.
  • **Project context** — `.cipherstash/context.json` records what `stash init` discovered: integration, package manager, env key names (never values), schemas, install command, CLI version. Treat it as authoritative.
  • **Action plan** — `.cipherstash/setup-prompt.md` is the project-specific to-do list for the current setup run. Read it first.

Invariants — never break these

1. **Encrypted columns are nullable `jsonb`.** Never declare them as `text`, `varchar`, `bytea`, or any plaintext type. Never mark them `NOT NULL` at creation — the application writes ciphertext after the column exists, and a `NOT NULL` constraint will break inserts. (DynamoDB equivalent: encrypted attributes are written as the SDK's encrypted-blob shape; do not invent your own scheme.) 2. **Never log plaintext.** Do not add `console.log`, `logger.info`, or test-fixture dumps that print decrypted values. Sensitive fields stay in memory only as long as the request needs them. Encrypted blobs are also not for logs — they reveal which records were touched. 3. **Never read or echo secrets.** Env key *names* (`CS_WORKSPACE_CRN`, `CS_CLIENT_ID`, `CS_CLIENT_KEY`, `CS_CLIENT_ACCESS_KEY`, `DATABASE_URL`) are fine to reference in code and docs. Their *values* are not. New env keys go in `.env.example` with placeholders; instruct the user to add the real value locally. Never read, `cat`, `grep`, or echo `~/.cipherstash/secretkey.json` (the development key), `~/.cipherstash/auth.json` (OAuth token and JWTs), anything under `~/.cipherstash/workspaces/`, or a value-bearing env file (`.env`, `.env.local`, `.env.production`, …). `.env.example` is the exception — it holds placeholders, not values, and you are expected to edit it. The CLI reads the credentials itself; no command needs you to open them. If a command fails on authentication, re-run `stash auth login` rather than inspecting the profile. 4. **Never invent CipherStash APIs.** If you don't know how a function is called, read the relevant skill (see below) — don't guess. The TypeScript types in `@cipherstash/stack` are the source of truth for what's callable. 5. **Never run database introspection yourself.** Don't run `psql`, `\d`, `pg_dump`, `supabase db dump`, or `drizzle-kit introspect`. The CLI already did this; the result is in `context.json`. If you need fresh introspection, ask the user to re-run `stash init`. 6. **Never modify these files.** `stash.config.ts` (generated by init — edits go in `.env`). `.cipherstash/` (CLI-owned). `~/.cipherstash/` (CLI-owned credentials — see invariant 3). The EQL schemas and functions installed by the CLI — `eql_v3`/`public.eql_v3_*` today, `eql_v2`/`eql_v2_*` on a legacy install. `stash eql upgrade` upgrades v3 only; legacy v2 state is diagnostic/read-only in current tooling. 7. **`@cipherstash/stack` must be excluded from any bundler.** The package wraps a native FFI module (`@cipherstash/protect-ffi`) that cannot be bundled. The moment you `npm install @cipherstash/stack` in a project with a bundler, configure the exclusion *before* writing any code that imports it. Concretely: Next.js needs `serverExternalPackages: ['@cipherstash/stack', '@cipherstash/protect-ffi']` in `next.config.{js,ts,mjs}`; webpack needs `externals` entries; esbuild needs `external`; Vite SSR needs `ssr.external`. Skipping this surfaces as `Cannot find module '@cipherstash/protect-ffi-*'` at runtime, often after the user has shipped to production. If you're declaring an encrypted column for the first time in a project, treat configuring this exclusion as part of the same change.

Migrations — three phases, always reversible

Encryption migrations affect production data. Treat every change as **plan → implement → verify**.

**Phase 1 — Plan.** Identify the candidate encrypted fields. Identify the migration tool already in use (Drizzle Kit, Supabase CLI, Prisma migrate, raw SQL). Produce a written migration plan including the rollback. Ask the user before applying anything that would change existing data.

**Phase 2 — Implement.** Add the encrypted column alongside the plaintext column (suffix `_encrypted` while both exist). Write a backfill script that reads the plaintext, encrypts via the encryption client, and writes the ciphertext. Keep the plaintext column readable during the transition; do not drop it in the same migration as the cutover.

**Phase 3 — Verify.** Run the migration in a dev or test database first. Confirm the round-trip: insert through the encryption client, select back, assert the value decrypts. Confirm the search operations declared o

Read more
Ships withstack

Searchable, application-level encryption for building privacy-first apps

Get the whole plugin