Skip to content

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

Description

@junkwd

What happened?

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";'
  for i in $(seq 1 60); do
    echo "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:

 [Bottom up (heavy) profile]:
   ticks parent  name
  10930   60.9%  UNKNOWN
   5782   52.9%    JS: *secondPhasePreProcess <bundle>/chunk-YSBB75DZ.js:314253:24   (minimatch)
   5782  100.0%      JS: ^preprocess          <bundle>/chunk-YSBB75DZ.js:314074:13
   5782  100.0%        JS: ^make               <bundle>/chunk-YSBB75DZ.js:314022:7
   5782  100.0%          JS: ^Minimatch         <bundle>/chunk-YSBB75DZ.js:313981:14
   5782  100.0%            JS: ~<anonymous>     <bundle>/chunk-YSBB75DZ.js:319581:34   (glob: patterns.map(p => new Minimatch(p, …)))

~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)

  1. 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.

  2. packages/cli/src/ui/hooks/atCommandProcessor.ts L62 / L96–99: AT_COMMAND_PATH_REGEX_SOURCE is

    (?:(?:"(?:[^"]*)")|(?:\\.|[^ \t\n\r,;!?()\[\]{}.]|\.(?!$|[ \t\n\r])))+
    

    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.

  3. That string is treated as a file path; resolveAtCommandPath fails (not_found), so L307–317 falls back to the glob tool with pattern: \**/${pathName}``.

  4. 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-expansion 5.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.

Related

What did you expect to happen?

Either of:

  • Piped stdin content is not parsed for @ commands in headless mode (or is parsed with escapePastedAtSymbols: true semantics), or
  • The @ 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
  • 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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area/coreIssues related to User Interface, OS Support, Core Functionalityeffort/medium2-3 days: UI state, async flow, cross-component refactorskind/bugpriority/p1Important and should be addressed in the near term.status/manual-triage

    Type

    Projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions