Skip to content

fix(cli): validate --name and pass --out in the v2 Drizzle migration generator#703

Merged
tobyhede merged 4 commits into
mainfrom
fix/drizzle-migration-injection
Jul 21, 2026
Merged

fix(cli): validate --name and pass --out in the v2 Drizzle migration generator#703
tobyhede merged 4 commits into
mainfrom
fix/drizzle-migration-injection

Conversation

@tobyhede

@tobyhede tobyhede commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

Closes #726

Closes #726

generateDrizzleMigration (the v2 Drizzle path in commands/db/install.ts) is a hand-copy of the v3 generator in commands/eql/migration.ts that never received its fixes. Two defects, both already solved on the v3 side.

Shell injection

install.ts built a shell string interpolating migrationName — straight from --name, unvalidated — and ran it through execSync. So:

stash eql install --drizzle --eql-version 2 --name 'x; rm -rf ~'

executed the injected command. The v3 generator guards the identical value with SAFE_MIGRATION_NAME = /^[\w-]+$/ and invokes via spawnSync(command, argv) with no shell. This applies the same guard and the same argv-based invocation.

SAFE_MIGRATION_NAME now lives in install.ts and migration.ts imports it — that direction was chosen deliberately: migration.ts already imports three helpers from install.ts, so the constant sits on an existing edge. The reverse would have created a cycle.

--out computed but never passed

install.ts computed outDir and searched it for the generated file, but omitted --out from the drizzle-kit command. A project whose drizzle.config.ts writes elsewhere had drizzle-kit write there while step 2 looked in drizzle/ — the run then died with a bare exit. The v3 generator passes --out=${outDir}; this does too, and adds the same "pass --out if your config writes elsewhere" hint on locate failure.

Also in this PR

The five process.exit(1) sites in this function became throw new CliExit(1) — per cli/exit.ts those were untracked by telemetry and untestable, which is why this path had no coverage.

That conversion needed one edit outside the two files: init/steps/install-eql.ts wraps installCommand in a broad catch that reports "EQL install failed" and lets init continue. Without an instanceof CliExit re-throw, a former hard exit would have become a misleading message plus continuation on the stash init --drizzle path.

Verification

  • New __tests__/generate-drizzle-migration.test.ts — 9 tests covering name rejection (; rm -rf ~, $(...), backticks, spaces, ../), rejection ordering vs the dry-run preview, argv shape, and CliExit on non-zero drizzle-kit exit.
  • Reverting both fixes fails 8 of 9. Restored: 9/9.
  • Full suite: 52 files, 592 tests passed. code:check exits 0.

Notes

  • SAFE_MIGRATION_NAME rejects ., so a name like install.eql that drizzle-kit previously accepted now errors. This matches v3 behaviour and the error names the allowed set. Widening both to /^[\w.-]+$/ is a one-line alternative if the narrowing isn't wanted.
  • Not fixed here, flagged for follow-up: v2 invokes drizzle-kit via runnerCommand (pnpm dlx/npx — download-and-run) where v3 uses execArgv (pnpm exec/npx --no-install — project-local). npx drizzle-kit can fetch a fresh drizzle-kit that won't resolve the project's drizzle.config.ts.
  • skills/stash-cli/SKILL.md updated: the --name rows now state the allowed character set, and --out documents that it reaches drizzle-kit --out.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes
    • Hardened Drizzle migration generation by rejecting unsafe --name values and preventing shell-interpolated execution.
    • Ensures --out is forwarded to Drizzle so generated files land where Drizzle expects them.
    • Improves failure handling and cleanup when generating or installing Drizzle migrations.
  • Documentation
    • Clarified valid --name formats and that --out must match the Drizzle configuration for eql install --drizzle and eql migration.
  • Tests
    • Added coverage for validation, command invocation, dry-run messaging, and error/cleanup scenarios.

…generator

`stash eql install --drizzle` (EQL v2) built a shell command string and ran it
via execSync, interpolating an unvalidated `--name`. A name carrying shell
metacharacters was executed:

    stash eql install --drizzle --eql-version 2 --name 'x; rm -rf ~'

It also computed `outDir` from `--out`, searched it for the generated file, but
never passed `--out` to drizzle-kit — so any project whose drizzle.config.ts
writes elsewhere had the migration written in one place and looked for in
another, dying at the locate step.

Both fixes mirror the v3 generator (`stash eql migration --drizzle`), which
already had them: reuse its `SAFE_MIGRATION_NAME` guard and invoke drizzle-kit
through spawnSync with an argv array (no shell). `SAFE_MIGRATION_NAME` moves to
install.ts — migration.ts already imports from it, so the shared constant sits
on that side of the existing edge and no import cycle is introduced.

The generator's five `process.exit(1)` sites become `throw new CliExit(1)` so
the branches are unit testable and the telemetry `finally` in main.ts still
runs. `install-eql.ts` re-throws CliExit from its broad catch, so a hard stop
during `stash init` stays a hard stop instead of being reframed as a database
connection failure.
@tobyhede
tobyhede requested a review from a team as a code owner July 20, 2026 02:47
@changeset-bot

changeset-bot Bot commented Jul 20, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 62b41d1

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 11 packages
Name Type
stash Patch
@cipherstash/basic-example Patch
@cipherstash/e2e Patch
@cipherstash/stack Patch
@cipherstash/stack-drizzle Patch
@cipherstash/stack-supabase Patch
@cipherstash/prisma-next Patch
@cipherstash/wizard Patch
@cipherstash/bench Patch
@cipherstash/test-kit Patch
@cipherstash/prisma-next-example Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@coderabbitai

coderabbitai Bot commented Jul 20, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The Drizzle migration generator now validates migration names, invokes project-local drizzle-kit with argv tokens, forwards --out, and propagates failures through `CliExit. Tests, documentation, and changeset notes cover the updated behavior.

Changes

Drizzle migration hardening

Layer / File(s) Summary
Generator validation and invocation
packages/cli/src/commands/db/install.ts, packages/cli/src/commands/eql/migration.ts
Adds shared safe-name validation, exports the v2 generator, invokes project-local drizzle-kit with argv-based --name and --out arguments, and updates the rendered migration command.
Failure handling and init propagation
packages/cli/src/commands/db/install.ts, packages/cli/src/commands/init/steps/install-eql.ts, packages/cli/src/commands/init/steps/__tests__/install-eql.test.ts
Replaces generator process exits with CliExit, logs failure details and cleanup errors, and preserves cooperative exits during EQL installation.
Validation coverage and command documentation
packages/cli/src/commands/db/__tests__/*, skills/stash-cli/SKILL.md, .changeset/khaki-pugs-play.md
Tests validation, invocation, dry-run output, failure reporting, output mismatches, and cleanup; documentation and release notes describe the updated flags and tooling behavior.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant CLI
  participant generateDrizzleMigration
  participant spawnSync
  participant drizzle-kit
  participant MigrationFilesystem
  CLI->>generateDrizzleMigration: provide --name and --out
  generateDrizzleMigration->>generateDrizzleMigration: validate --name
  generateDrizzleMigration->>spawnSync: invoke with argv tokens
  spawnSync->>drizzle-kit: run project-local generate command
  drizzle-kit->>MigrationFilesystem: write migration under --out
  generateDrizzleMigration->>MigrationFilesystem: locate and rewrite migration
  MigrationFilesystem-->>CLI: generated migration or CliExit
Loading

Possibly related issues

Possibly related PRs

  • cipherstash/stack#687 — Overlaps the init/EQL error-handling flow around generated Drizzle migrations.
  • cipherstash/stack#691 — Directly overlaps name validation, argv invocation, --out forwarding, and shared validation.

Suggested reviewers: freshtonic

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly reflects two core changes in the v2 Drizzle migration generator: --name validation and forwarding --out.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/drizzle-migration-injection

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@tobyhede
tobyhede requested a review from freshtonic July 20, 2026 03:00
Review follow-ups on the v2 Drizzle migration generator fix.

The `instanceof CliExit` re-throw in `init/steps/install-eql.ts` was the
one edit outside the two main files and had no coverage. Without it a
hard stop the installer already reported on gets reframed as "check your
database connection" and init continues past it. Add a test for the
re-throw plus its companion — an ordinary throw still being swallowed so
init carries on — since the pair is what makes either meaningful.

Tighten the argv assertion from `typeof command === 'string'` and
`toContain` to a full `toEqual`. The loose form passed even with the
runner prefix dropped, which would run drizzle-kit under the wrong
resolver. `detectPackageManager` is pinned so the assertion is
deterministic (detection reads the cwd lockfile and
npm_config_user_agent); the runner mapping itself stays real.

Also correct the stale doc comment on `installEqlStep` — `installCommand`
now ends init via `process.exit(1)` or a thrown `CliExit`, not only the
former.

Verified by revert: removing the re-throw fails 1 of 7; dropping the
`dlx` prefix fails 1 of 9. Suite: 52 files, 594 tests. code:check clean.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR hardens the v2 (stash eql install --drizzle --eql-version 2) Drizzle migration generator to match the already-fixed v3 generator, addressing a shell-injection vector in --name, ensuring --out is actually passed to drizzle-kit, and converting hard process.exit(1) paths into CliExit to preserve telemetry and enable unit testing.

Changes:

  • Validate --name with a shared SAFE_MIGRATION_NAME regex and invoke drizzle-kit via spawnSync(command, argv) (no shell interpolation) in the v2 generator.
  • Always forward --out to drizzle-kit generate, and improve the locate-failure hint when the configured output directory differs.
  • Replace several process.exit(1) paths with throw new CliExit(1) and add targeted unit coverage for both the migration generator and init’s CliExit rethrow behavior.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated no comments.

Show a summary per file
File Description
skills/stash-cli/SKILL.md Documents the tightened --name character set and clarifies that --out is forwarded to drizzle-kit --out.
packages/cli/src/commands/init/steps/install-eql.ts Re-throws CliExit so init does not reframe cooperative exits as connection failures or continue past hard stops.
packages/cli/src/commands/init/steps/tests/install-eql.test.ts Adds tests verifying CliExit is re-thrown and non-CliExit errors are still swallowed with a generic message.
packages/cli/src/commands/eql/migration.ts Removes the local copy of SAFE_MIGRATION_NAME and imports the shared constant from the v2 install module.
packages/cli/src/commands/db/install.ts Fixes v2 Drizzle migration generation: validates --name, passes --out, uses spawnSync argv invocation, and throws CliExit on failures.
packages/cli/src/commands/db/tests/generate-drizzle-migration.test.ts New unit tests covering unsafe-name rejection, dry-run ordering/preview, argv shape (no shell), --out forwarding, and non-zero drizzle-kit exit handling.
.changeset/khaki-pugs-play.md Adds a patch changeset for the stash package describing the two v2 generator fixes.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@tobyhede

Copy link
Copy Markdown
Contributor Author

Relates to #707 (EQL v2 removal). This patches generateDrizzleMigration, the v2 Drizzle install path — code #707 deletes outright. Landing this fix now closes a shipping shell-injection hole; #707 later removes the surface entirely. Noting the relationship so the removal doesn't silently un-fix it, and so this isn't mistaken for v3 work.

Also adjacent to #613 (v3 Drizzle migration install) — same command neighbourhood, opposite generation.

@auxesis auxesis left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verdict

The fix itself is sound. spawnSync with an argv array, no shell, gated on ^[\w-]+$ is the right shape for the injection defect, and the exact-argv assertion (rather than toContain) genuinely pins --out reaching drizzle-kit. Hoisting SAFE_MIGRATION_NAME into db/install.ts with the import-cycle rationale spelled out is the right call. Changeset and skills/stash-cli/SKILL.md are both updated. No blocking defects found.

Everything below is coverage or a design question, not a bug in the fix.

The theme: generateDrizzleMigration converted five process.exit(1) sites into throw new CliExit(1); only two are covered (bad name, non-zero exit). The three cleanup/abort paths — locate failure, SQL-load failure, write failure — are untested, even though the sibling v3 suite (packages/cli/src/commands/eql/__tests__/migration.test.ts:219) already tests the equivalent write-failure cleanup. Those paths matter more than usual here: a swallowed or mis-ordered CliExit leaves an empty scaffolded migration on disk that drizzle-kit migrate will happily apply and record as done.

The one item I'd genuinely resolve before merge is the runnerArgv / execArgv divergence (inline on install.ts:515) — not a regression, but the new exact-argv assertion cements it.

Caveat: the suite could not be executed in this checkout (vitest is not installed — pnpm install was never run here), so every finding is from reading, and the suggested test sketches are unexecuted.

Additional findings not posted inline

  • Nothing asserts the two generators share one SAFE_MIGRATION_NAME. The new doc comment says "there must not be a second copy", but that's unenforced — a re-introduced local copy in commands/eql/migration.ts would pass CI silently. A one-line expect(SAFE_MIGRATION_NAME.test('a b')).toBe(false) imported from db/install.js in the v3 suite would make the drift fail.
  • The bundled-SQL failure branch (install.ts:609) is the twin of the --latest branch flagged inline. Stubbing loadBundledEqlSql gets you both cleanup arms for one mock, so this is nearly free once the --latest test exists.

Review stats

Source Model Type Raw Kept
claude claude-opus-4-8 test-gap 6 6 (+1 promoted from its summary note)
codex gpt-5.5 test-gap 1 1 (merged into the write-failure finding)
  • Total raw: 7 → 7 kept (7 inline, 2 overflow bullets in the body).
  • Dropped as unsubstantiated: 0. Every finding verified against the diff and the surrounding code.
  • Cross-model overlap: 1 kept finding corroborated by 2+ models — the write-failure cleanup gap (install.ts:629), raised independently by both sources with near-identical test sketches. The remaining 6 are single-source (claude), all verified.
  • One claude finding was promoted from its summary's "out-of-scope note" to an inline comment (install.ts:515, the runnerArgv vs execArgv divergence) — it verifies cleanly against the v3 generator's own comment and is the highest-value item in the set.

Comment thread packages/cli/src/commands/db/install.ts
Comment thread packages/cli/src/commands/db/install.ts Outdated
Comment thread packages/cli/src/commands/db/install.ts
Comment thread packages/cli/src/commands/db/install.ts
Comment thread packages/cli/src/commands/db/install.ts
Comment thread packages/cli/src/commands/db/install.ts
Comment thread packages/cli/src/commands/db/install.ts
tobyhede added 2 commits July 21, 2026 11:15
…execArgv

Address the @auxesis review threads on #703 — all test-coverage gaps on the
v2 generateDrizzleMigration failure arms, plus one documentation gap:

- write-failure cleanup (delegating writeFileSync spy)
- --latest fetch-failure cleanup (stubbed downloadEqlSql)
- the --out remediation hint when the migration isn't where we looked
- the defaulted --out arm (flag omitted -> <cwd>/drizzle) via dry-run
- the ENOENT and exit-status arms of the error fallback chain
- --name '' rejection boundary (empty is not nullish)

Also comment why v2 keeps the download-and-run form (dlx) while v3 uses the
project-local execArgv form, so the pinned-argv divergence is a conscious
contract.
Switch the v2 generateDrizzleMigration invocation from the download-and-run
form (runnerArgv -> pnpm dlx / npx / bunx) to the project-local form
(execArgv -> pnpm exec / npx --no-install), so drizzle-kit resolves THIS
project's drizzle.config.ts and schema and fails loudly if drizzle-kit is
missing instead of surprise-downloading a possibly-different major. The
dry-run preview (execCommand) and the drizzle-kit migrate hint follow suit.
Updates the pinned-argv test from 'dlx' to 'exec'. Resolves review thread T2
by making the recommended change rather than documenting the divergence.

@auxesis auxesis left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for addressing the feedback @tobyhede ❤️

@tobyhede
tobyhede merged commit f0b20d3 into main Jul 21, 2026
10 checks passed
@tobyhede
tobyhede deleted the fix/drizzle-migration-injection branch July 21, 2026 03:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

stash eql install --drizzle: --name reaches execSync unvalidated (shell injection) + --out never passed

3 participants