Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions .changeset/stash-cli-skill-refresh.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
---
"stash": patch
---

Refresh the bundled `stash-cli` agent skill and the CLI README against the current
command surface. The skills directory ships inside the `stash` tarball and is copied
into the user's `.claude/skills/` / `.codex/skills/` (or inlined into `AGENTS.md`) at
handoff time, so a stale skill becomes stale guidance in the user's project.

- **New `Start here` and `Authentication` sections.** Setup is driven through the CLI:
agents read `stash manifest --json` first, then trigger `stash auth login --json` and
surface the verification URL for a human to approve, then run `stash init`. Authenticating
before `init` matters — `init`'s auth step is interactive and would otherwise try to open
a browser on the agent's host.
- **New `Never read these` invariant**, mirrored into the `AGENTS.md` doctrine: agents must
never read `~/.cipherstash/secretkey.json`, `~/.cipherstash/auth.json`, anything under
`~/.cipherstash/workspaces/`, or `.env*`. The wizard already blocks these paths in code;
the other handoff targets had no written rule.
- **Documents `manifest`, `doctor`, `wizard`, and `auth regions`**, which the skill omitted
entirely, plus the non-interactive interface (per-command escape hatches, exit codes, the
`DATABASE_URL` resolution order, the `auth login --json` NDJSON event contract).
- **Corrects the `db``eql` move.** `db install`, `db upgrade`, and `db status` are
deprecated aliases that warn and forward; `db push`, `db activate`, `db validate`,
`db test-connection`, and `db migrate` remain in the `db` group.
- **Scopes `db push` / `db activate` as EQL v2 + CipherStash Proxy only**, in both the skill
and the README's recommended flow. SDK users hold their encryption config in application
code and don't need them.
- Adds the missing `--database-url`, `--eql-version`, `--prisma-next`, `--proxy`/`--no-proxy`,
and `--region` flags; corrects six programmatic API signatures; fixes the README's claim
that `stash init` ends in an agent-handoff menu (that belongs to `stash plan` / `stash impl`);
and marks `stash env` as the non-functional stub it currently is.
18 changes: 18 additions & 0 deletions .changeset/wizard-allow-env-templates.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
---
"@cipherstash/wizard": patch
---

Stop the agent guard from blocking `.env.example`.

`SENSITIVE_FILE_PATTERNS` matched `/\.env($|\.)/`, which tests true against
`.env.example`. Because the guard covers `Edit` and `Write` as well as `Read`,
the wizard's agent was blocked from creating or editing the very file the
CipherStash doctrine tells it to write ("New env keys go in `.env.example` with
placeholders"). Committed env templates carry placeholder key names, not values.

`.env.example`, `.env.sample` and `.env.template` are now readable and writable.
Everything else is unchanged: `.env`, `.env.local`, `.env.production`, and
value-bearing files that merely start with a template name
(`.env.example.local`, `.env.example.bak`) stay blocked, as do `auth.json`,
`secretkey.json` and credential files. Bash access to any env file — including
the templates — remains blocked; `Read`/`Write` is the sanctioned path.
23 changes: 15 additions & 8 deletions packages/cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,32 +12,39 @@ The single CLI for CipherStash. It handles authentication, project initializatio
```bash
npm install -D stash
npx stash auth login # authenticate with CipherStash
npx stash init # scaffold, introspect, install EQL, hand off to your agent
npx stash init # scaffold, introspect, install EQL
```

`stash init` runs the whole setup as one flow: authenticate, resolve `DATABASE_URL`, introspect your database and let you pick which columns to encrypt, install dependencies, install the EQL extension, and finish by handing off to your local coding agent. At the end it presents a four-option menu:
`stash init` does the scaffold-once work as one flow: authenticate, resolve `DATABASE_URL`, choose Proxy or direct SDK access, introspect your database and scaffold an encryption client, install dependencies, install the EQL extension, and write `.cipherstash/context.json`. It stops there, at a clean save-point, and offers to continue into `stash plan`.

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.json` and `setup-prompt.md`, then launches `claude` interactively.
- **Hand off to Codex** — copies the same skills into `.codex/skills/`, writes a sentinel-managed `AGENTS.md` (durable doctrine), plus `.cipherstash/` context files, then launches `codex`.
- **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.md` with the doctrine *plus* the relevant skill content inlined under a sentinel block, and stops.

A project-specific action plan is written to `.cipherstash/setup-prompt.md` regardless of which option 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`.
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, init still writes the rules files and prints install instructions — your progress is never wasted.
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, hands off to your agent
└── Agent edits schema files / generates migrations
└── npx stash db push ← when ready to roll out further changes
└── 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/validate/push/migrate, schema introspection, and a `stash wizard` subcommand that thin-wraps [`@cipherstash/wizard`](https://www.npmjs.com/package/@cipherstash/wizard). The wizard package itself is a separate npm install — kept out of the `stash` bundle so the agent SDK doesn't bloat the CLI.
`stash` covers authentication, initialization, EQL install/upgrade/status, schema introspection, the encryption rollout and cutover commands, and a `stash wizard` subcommand that thin-wraps [`@cipherstash/wizard`](https://www.npmjs.com/package/@cipherstash/wizard). The wizard package itself is a separate npm install — kept out of the `stash` bundle so the agent SDK doesn't bloat the CLI.

> `stash db push` is **not** part of the default flow. It registers the encryption config in `public.eql_v2_configuration`, which only [CipherStash Proxy](https://github.com/cipherstash/proxy) reads. SDK users (Drizzle, Supabase, plain PostgreSQL) keep that config in application code and can skip it.

---

Expand Down
4 changes: 2 additions & 2 deletions packages/cli/src/commands/init/doctrine/AGENTS-doctrine.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,10 @@ This document is the **durable rule book** for any agent working on this codebas

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.
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). The `eql_v2` schema and `eql_v2_*` functions (CLI-managed; missing function ⇒ `stash eql upgrade`, not a hand-edit).
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_v2` schema and `eql_v2_*` functions (CLI-managed; missing function ⇒ `stash eql upgrade`, not a hand-edit).
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
Expand Down
36 changes: 36 additions & 0 deletions packages/wizard/src/__tests__/interface.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,42 @@ describe('wizardCanUseTool', () => {
})
})

describe('.env templates are not sensitive', () => {
// The doctrine tells the agent to add placeholder keys to `.env.example`.
// The guard covers Edit and Write as well as Read, so a blanket `.env.`
// rule made the file the agent is told to write unreachable.
it.each(['.env.example', '.env.sample', '.env.template'])(
'allows Read/Edit/Write/Glob on %s',
(name) => {
expect(wizardCanUseTool('Read', { file_path: `/project/${name}` })).toBe(
true,
)
expect(wizardCanUseTool('Edit', { file_path: `/project/${name}` })).toBe(
true,
)
expect(
wizardCanUseTool('Write', { file_path: `/project/${name}` }),
).toBe(true)
expect(wizardCanUseTool('Glob', { pattern: name })).toBe(true)
},
)

it('still blocks value-bearing files that only start with a template name', () => {
expect(
wizardCanUseTool('Read', { file_path: '/project/.env.example.local' }),
).toContain('blocked')
expect(
wizardCanUseTool('Write', { file_path: '/project/.env.example.bak' }),
).toContain('blocked')
})

it('still blocks env templates from Bash — Read/Write is the sanctioned path', () => {
expect(
wizardCanUseTool('Bash', { command: 'cat .env.example' }),
).toContain('.env')
})
})

describe('Bash commands', () => {
it('allows allowlisted npm commands', () => {
expect(
Expand Down
16 changes: 14 additions & 2 deletions packages/wizard/src/agent/interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,17 @@ const ALLOWED_WRITE_PATHS = [

/** Sensitive file patterns the agent must not read directly. */
const SENSITIVE_FILE_PATTERNS = [
/\.env($|\.)/, // .env, .env.local, .env.production, etc.
// `.env`, `.env.local`, `.env.production`, … but not the committed templates
// `.env.example` / `.env.sample` / `.env.template`. Those carry placeholder
// key names rather than values, and the agent doctrine
// (`packages/cli/src/commands/init/doctrine/AGENTS-doctrine.md`, invariant 3)
// instructs the agent to create and edit them — a blanket `.env.` rule made
// that impossible, since this guard also covers Edit and Write.
//
// The negative lookahead is anchored, so a value-bearing file that merely
// starts with a template name (`.env.example.bak`, `.env.example.local`)
// stays blocked.
/\.env($|\.(?!(example|sample|template)$))/,
/auth\.json$/, // ~/.cipherstash/auth.json
/secretkey\.json$/, // ~/.cipherstash/secretkey.json
/credentials/i, // Various credential files
Expand Down Expand Up @@ -178,7 +188,9 @@ export function wizardCanUseTool(
return 'Multi-line commands are not allowed for security reasons.'
}

// Block direct .env access via Bash
// Block direct .env access via Bash. Deliberately stricter than the
// file-path guard above: `.env.example` is reachable through Read/Edit/Write,
// so there is no reason for the agent to touch any env file from a shell.
if (/\.(env|env\.local)/.test(command)) {
return 'Direct .env file access via Bash is blocked. Use the wizard-tools MCP server instead.'
}
Expand Down
Loading
Loading