fix(hooks): cover brace groups + yarn-run/bun-bare dev variants

Two false-negatives surfaced in PR #1889 review:

1. Brace-group bypass (Greptile).
   `{ npm run dev; }` evaluates the dev command in the *current*
   shell — semantically distinct from `( ... )` but with the same
   effect for this hook. `splitShellSegments` correctly cleaves the
   group at `;` into `["{ npm run dev", "}"]`, but the first segment's
   leading token under `readToken` is the bare `{`, which was not in
   `DEV_COMMAND_WORDS`, so the dev-pattern check was skipped.

   Fix: treat `{` and `}` as no-op tokens in `getLeadingCommandWord`
   so we keep walking to the real command word. Matches how shell
   itself parses brace groups (the braces are reserved words, not
   commands). Bash requires a space after `{` and a terminator before
   `}` for an actual group, so `{npm run dev}` correctly remains
   allowed (single token `{npm`, not in `DEV_COMMAND_WORDS`).

2. Missing yarn-run / bun-bare variants (CodeRabbit).
   Both `yarn dev` *and* `yarn run dev` are valid (the latter is what
   `package.json` actually wires `dev` to under yarn 1.x). The same
   `(run )?` symmetry applies to bun. The previous `DEV_PATTERN` only
   matched `yarn\s+dev` and `bun\s+run\s+dev`, allowing the cross
   forms to pass through silently.

   Fix: `yarn(?:\s+run)?\s+dev` and `bun(?:\s+run)?\s+dev` — same
   shape `pnpm(?:\s+run)?\s+dev` was already using.

Verified after this commit (every form now exits 2):

  { npm run dev; }
  { npm run dev ; }
  echo hi && { npm run dev; }
  ({ npm run dev; })
  $( { npm run dev; } )
  yarn run dev
  bun dev

Verified still allowed (no regression):

  echo "{ npm run dev; }"   # literal inside double quotes
  {npm run dev}             # not a brace group per bash syntax
This commit is contained in:
Jamkris
2026-05-14 12:23:55 +09:00
parent 70b86d81c4
commit e2eaf4ac2f

View File

@@ -127,6 +127,8 @@ function getLeadingCommandWord(segment) {
continue;
}
if (token === '{' || token === '}') continue;
if (/^[A-Za-z_][A-Za-z0-9_]*=.*/.test(token)) continue;
const normalizedToken = normalizeCommandWord(token);
@@ -159,7 +161,7 @@ process.stdin.on('data', chunk => {
});
const TMUX_LAUNCHER = /^\s*tmux\s+(new|new-session|new-window|split-window)\b/;
const DEV_PATTERN = /\b(npm\s+run\s+dev|pnpm(?:\s+run)?\s+dev|yarn\s+dev|bun\s+run\s+dev)\b/;
const DEV_PATTERN = /\b(npm\s+run\s+dev|pnpm(?:\s+run)?\s+dev|yarn(?:\s+run)?\s+dev|bun(?:\s+run)?\s+dev)\b/;
/**
* Collect every command-line segment we should evaluate. Returns the top-level