You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Headless -p with piped stdin hangs at 100% CPU (uninterruptible) when input contains "@scope/pkg" followed by quoted strings — @-command regex swallows the text, glob fallback stalls in minimatch #29434
In headless mode (-p with content piped on stdin), the CLI hangs before sending any request to the model when the piped text contains an @-prefixed token that is immediately followed by a double quote and then more quoted strings — the most common case being source code with a scoped npm import such as import { x } from "@scope/pkg"; followed by other import … from "…" lines.
Symptoms:
No output, no error, no timeout. --output-format stream-json emits only the init event and nothing else.
The node process sits at ~100% CPU (state RN) indefinitely.
SIGINT / SIGTERM are ignored (the event loop is blocked by a synchronous loop); only SIGKILL stops it.
Reproduced with v0.59.0 (volta install) and v0.60.0 (npx -y @google/[email protected]). The relevant code is unchanged on main (cfbcaa8).
Independent of workspace size (reproduced in a directory containing a single tiny file), model (-m gemini-3.6-flash, also 3.5-flash / 3.5-pro) and --approval-mode (plan, default, yolo all hang).
The same text sent directly to the Vertex AI streamGenerateContent endpoint (with the CLI's own system prompt and session_context copied from local telemetry, the same sampling params, and thinking enabled) returns in 3–6 s, so the model/backend is not involved.
Minimal reproduction (no real project needed)
mkdir -p /tmp/gcli-repro &&cd /tmp/gcli-repro &&echo x > a.txt
# 1 scoped import + 60 ordinary imports (4.2 KB). No file named "scope/pkg" exists anywhere.
{ echo'import { useThing } from "@scope/pkg";'foriin$(seq 1 60);doecho"import { alpha$i, beta$i, gamma$i } from \"~/modules/feature$i/index\";"done; } > repro.txt
# HANGS (100% CPU, never returns):
cat repro.txt | gemini --approval-mode plan -p "Summarize the content above in one sentence."# Control A – escape the "@": returns in ~4 s
sed 's/@/\\@/g' repro.txt | gemini --approval-mode plan -p "Summarize the content above in one sentence."# Control B – drop the single "@scope/pkg" line: returns in ~5 s
tail -n +2 repro.txt | gemini --approval-mode plan -p "Summarize the content above in one sentence."
(--skip-trust and --allowed-mcp-server-names <none> were also passed in my runs to keep the run non-interactive and to avoid loading local MCP servers; they do not change the outcome.)
Where the time goes (V8 tick profile of the hung process)
GEMINI_CLI_NO_RELAUNCH=1 node --prof --max-old-space-size=8192 <bundle>/gemini.js … on the repro above, killed after 20 s:
~97 % of all ticks are inside minimatch's pattern preprocessing (glob 12.0.0 / minimatch 10.2.5 / brace-expansion 5.0.5 per the v0.60.0 lockfile), called from the Glob constructor used by the glob tool. I also patched a copy of the bundle to log the pattern handed to new Minimatch(...): it is exactly `**/*${pathName}*` with the 4,175-char swallowed text (60 { … } groups, 182 /).
Root cause (as far as I can tell from main @ cfbcaa8)
packages/cli/src/nonInteractiveCli.ts L281–288: in headless mode the whole stdin + prompt string is passed to handleAtCommand({ query: input, …, escapePastedAtSymbols: false }), so piped file contents are parsed for @ commands.
packages/cli/src/ui/hooks/atCommandProcessor.ts L62 / L96–99: AT_COMMAND_PATH_REGEX_SOURCE is
The first alternative ("(?:[^"]*)", meant for quoted paths) is allowed anywhere inside the path, not just at its start. When the path is followed by a " (as in "@scope/pkg"), that alternative matches from the closing quote to the next quote in the text, then the other alternatives continue, then the next quoted span is swallowed, and so on. On the repro above the single @ match is 4,175 characters long (@scope/pkg";\nimport { alpha1, beta1, gamma1 } from "~/modules/feature1/index";\nimport …) — the whole rest of the input, newlines included.
That string is treated as a file path; resolveAtCommandPath fails (not_found), so L307–317 falls back to the glob tool with pattern: \**/${pathName}``.
The resulting glob pattern is a multi-kilobyte string containing dozens of { a, b, c } brace groups (every import { … } from in the swallowed source becomes one) and / separators. brace-expansion5.0.5 (the version in the v0.60.0 lockfile) expands it up to its EXPANSION_MAX = 100_000 result cap, and minimatch's preprocess → secondPhasePreProcess (optimizationLevel 2, set by glob) then walks those 100,000 patterns × ~180 segments synchronously on the main thread. Reproduced standalone, outside the CLI, with the same pattern and the CLI's exact glob() options (nocase, dot, nodir, stat, withFileTypes, ignore):
glob
minimatch
brace-expansion
braceExpand() results
await glob()
12.0.0
10.2.5
5.0.5 (bundled)
100,000 (cap)
not finished after 90 s
12.0.0
10.2.5
5.0.12
1,334
~3 s
brace-expansion ≥ 5.0.9 adds EXPANSION_MAX_LENGTH = 4_000_000 (its source cites CVE-2026-14257), which is what stops the expansion early. So with current dependencies a single stray @ token costs ~3 s of CPU, and with the bundled 5.0.5 it effectively never returns (I let the CLI run for >4 minutes on a real-world file, >60 s on the repro above).
Because the loop is synchronous, the signal passed to handleAtCommand and the process signal handlers never get a chance to run.
Before the glob fallback, a path that contains newlines (or exceeds a sane length) is rejected instead of being turned into a **/*…* pattern — which would also bound the worst case for any other malformed input.
At minimum, a "file not found" style error would be preferable to a silent 100 % CPU hang that cannot be interrupted.
Bumping brace-expansion to ≥ 5.0.9 would turn the hang into a ~3 s stall (see the table above), but the parser fix is what actually removes the problem — the swallowed text should never reach the glob tool.
Client information
CLI Version: 0.59.0 (volta, @google/gemini-cli) and 0.60.0 (npx -y @google/[email protected])
Node: v22.21.1
OS: macOS 26.6.2 (Darwin 25.6.0), arm64
Model: gemini-3.6-flash (also reproduced with gemini-3.5-flash and gemini-3.5-pro)
Sandbox: none
Login information
Vertex AI (GOOGLE_GENAI_USE_VERTEXAI=true + GOOGLE_CLOUD_PROJECT / GOOGLE_CLOUD_LOCATION=global).
Anything else we need to know?
Workaround for users piping source code into gemini -p: escape every @ in the piped text (sed 's/@/\\@/g'); the (?<!\\)@ lookbehind then skips it. With that, a 342 KB Vue file containing 69 @ tokens that previously hung is summarised in ~5–15 s.
Only stdin/prompt text matters; the same content read via @file reference (i.e. the intended use of the feature) is unaffected because the regex is applied to the short prompt, not the file body.
Edited after filing: refined step 4 of the root cause (brace-expansion version and the standalone glob() measurements) and tightened a few wordings; the reproduction and conclusions are unchanged.
What happened?
In headless mode (
-pwith content piped on stdin), the CLI hangs before sending any request to the model when the piped text contains an@-prefixed token that is immediately followed by a double quote and then more quoted strings — the most common case being source code with a scoped npm import such asimport { x } from "@scope/pkg";followed by otherimport … from "…"lines.Symptoms:
--output-format stream-jsonemits only theinitevent and nothing else.RN) indefinitely.SIGINT/SIGTERMare ignored (the event loop is blocked by a synchronous loop); onlySIGKILLstops it.npx -y @google/[email protected]). The relevant code is unchanged onmain(cfbcaa8).-m gemini-3.6-flash, also 3.5-flash / 3.5-pro) and--approval-mode(plan,default,yoloall hang).The same text sent directly to the Vertex AI
streamGenerateContentendpoint (with the CLI's own system prompt andsession_contextcopied from local telemetry, the same sampling params, and thinking enabled) returns in 3–6 s, so the model/backend is not involved.Minimal reproduction (no real project needed)
(
--skip-trustand--allowed-mcp-server-names <none>were also passed in my runs to keep the run non-interactive and to avoid loading local MCP servers; they do not change the outcome.)Where the time goes (V8 tick profile of the hung process)
GEMINI_CLI_NO_RELAUNCH=1 node --prof --max-old-space-size=8192 <bundle>/gemini.js …on the repro above, killed after 20 s:~97 % of all ticks are inside minimatch's pattern preprocessing (
glob12.0.0 /minimatch10.2.5 /brace-expansion5.0.5 per the v0.60.0 lockfile), called from theGlobconstructor used by theglobtool. I also patched a copy of the bundle to log the pattern handed tonew Minimatch(...): it is exactly`**/*${pathName}*`with the 4,175-char swallowed text (60{ … }groups, 182/).Root cause (as far as I can tell from
main@ cfbcaa8)packages/cli/src/nonInteractiveCli.tsL281–288: in headless mode the wholestdin + promptstring is passed tohandleAtCommand({ query: input, …, escapePastedAtSymbols: false }), so piped file contents are parsed for@commands.packages/cli/src/ui/hooks/atCommandProcessor.tsL62 / L96–99:AT_COMMAND_PATH_REGEX_SOURCEisThe first alternative (
"(?:[^"]*)", meant for quoted paths) is allowed anywhere inside the path, not just at its start. When the path is followed by a"(as in"@scope/pkg"), that alternative matches from the closing quote to the next quote in the text, then the other alternatives continue, then the next quoted span is swallowed, and so on. On the repro above the single@match is 4,175 characters long (@scope/pkg";\nimport { alpha1, beta1, gamma1 } from "~/modules/feature1/index";\nimport …) — the whole rest of the input, newlines included.That string is treated as a file path;
resolveAtCommandPathfails (not_found), so L307–317 falls back to the glob tool withpattern: \**/${pathName}``.The resulting glob pattern is a multi-kilobyte string containing dozens of
{ a, b, c }brace groups (everyimport { … } fromin the swallowed source becomes one) and/separators.brace-expansion5.0.5 (the version in the v0.60.0 lockfile) expands it up to itsEXPANSION_MAX = 100_000result cap, and minimatch'spreprocess→secondPhasePreProcess(optimizationLevel 2, set byglob) then walks those 100,000 patterns × ~180 segments synchronously on the main thread. Reproduced standalone, outside the CLI, with the same pattern and the CLI's exactglob()options (nocase,dot,nodir,stat,withFileTypes,ignore):braceExpand()resultsawait glob()brace-expansion ≥ 5.0.9 adds
EXPANSION_MAX_LENGTH = 4_000_000(its source cites CVE-2026-14257), which is what stops the expansion early. So with current dependencies a single stray@token costs ~3 s of CPU, and with the bundled 5.0.5 it effectively never returns (I let the CLI run for >4 minutes on a real-world file, >60 s on the repro above).Because the loop is synchronous, the
signalpassed tohandleAtCommandand the process signal handlers never get a chance to run.Related
RangeError: Maximum call stack size exceededinhandleAtCommand, priority/p1) — same regex, different failure mode; closed as stale / not planned.@filename:lineor@filename:rangesyntax #19985 is a different root cause (InputPrompt line wrapping), listed only to avoid confusion.What did you expect to happen?
Either of:
@commands in headless mode (or is parsed withescapePastedAtSymbols: truesemantics), or@path regex stops at the closing quote / only honours the quoted form when the quote immediately follows@(which is what fix(at-command): prevent stack overflow from regex backtracking on large inputs #27580 does with an iterative scanner), and**/*…*pattern — which would also bound the worst case for any other malformed input.At minimum, a "file not found" style error would be preferable to a silent 100 % CPU hang that cannot be interrupted.
Bumping
brace-expansionto ≥ 5.0.9 would turn the hang into a ~3 s stall (see the table above), but the parser fix is what actually removes the problem — the swallowed text should never reach the glob tool.Client information
Login information
Vertex AI (
GOOGLE_GENAI_USE_VERTEXAI=true+GOOGLE_CLOUD_PROJECT/GOOGLE_CLOUD_LOCATION=global).Anything else we need to know?
gemini -p: escape every@in the piped text (sed 's/@/\\@/g'); the(?<!\\)@lookbehind then skips it. With that, a 342 KB Vue file containing 69@tokens that previously hung is summarised in ~5–15 s.@filereference (i.e. the intended use of the feature) is unaffected because the regex is applied to the short prompt, not the file body.Edited after filing: refined step 4 of the root cause (brace-expansion version and the standalone
glob()measurements) and tightened a few wordings; the reproduction and conclusions are unchanged.