Skip to content

fix(core): eliminate false positives on untrusted command flags and compound loops - #29672

Merged
DavidAPierce merged 10 commits into
mainfrom
fix/untrusted-flags-false-positives
Oct 8, 2026
Merged

DavidAPierce merged 10 commits into
mainfrom
fix/untrusted-flags-false-positives

Conversation

@DavidAPierce

Copy link
Copy Markdown
Contributor

Summary

Eliminates false-positive security warnings and confirmation halts during shell command execution caused by:

  1. Shell variable expansion issues and overly broad token indexing in untrustedContextTracker.
  2. Harmless POSIX navigation and inspection flags (ls -ld, grep -rn, git -C, git log --all --grep --oneline) being flagged as tainted arguments.
  3. Compound commands and loops (for repo in ...; do ...; done or chained commands) being blocked by catch-all ASK_USER policies even when every decomposed sub-command explicitly matches an ALLOW rule.

Details

  • Shell Variable Preservation: Configures shellParse in findUntrustedFlags with a custom variable resolver (key) => \${key}`so shell variables (e.g."$repo") do not expand to empty strings and shift positional token parsing. Skips variable tokens starting with $` from argument matching.
  • Safe Flag Exemption List: Adds SAFE_SHORT_FLAGS and SAFE_LONG_FLAGS sets covering read-only, formatting, and directory navigation flags (-C, -la, -ld, -rn, --all, --grep, --oneline, etc.) that cannot execute arbitrary code or escalate privileges on their own.
  • Case-Sensitive Short Flag Handling: Evaluates HIGH_RISK_VALUE_FLAGS against raw flag names for short flags and lowercased names only for long flags (--...), preventing safe uppercase flags like -C from colliding with lowercase -c.
  • Targeted Argument Matching: Restricts untrusted argument matching to high-risk patterns (URLs, absolute paths, script extensions) and stops indexing path segments by splitting on slashes, preventing normal relative workspace file paths (e.g. src/index.ts, package.json) and file mode listings from triggering taint warnings.
  • Safe Flag Value Substring Guard: Guards isHighRiskValSubstring with (!isSafeFlag || isHighRiskFlag) to ensure safe flags taking URLs or paths as search patterns (e.g., git log --grep="http://...") are not flagged.
  • Shell Metadata Stripping: Strips internal ShellTool execution wrappers (Output:, Exit Code:, PGID:) from indexed tokens with leading newline/whitespace resilience.
  • PolicyEngine Compound Command Upgrade: When evaluating compound commands or loops, decomposes sub-commands and upgrades a fallback catch-all ASK_USER decision to ALLOW if every decomposed sub-command matches an explicit ALLOW rule without redirection downgrade.

Related Issues

Fixes #29579
Fixes #29650

How to Validate

  1. Run the targeted unit test suites:
    npm test -w @google/gemini-cli-core -- src/policy/policy-engine.test.ts src/utils/untrustedContextTracker.test.ts
  2. Verify full type checking and linting pass:
    npm run typecheck && npm run lint
  3. Test commands in the CLI that previously triggered security warnings following command outputs containing paths, file modes, or shell loops:
    • for repo in dir1 dir2; do git -C "$repo" log --all --grep="11541" --oneline; done
    • git -C <dir> log --oneline
    • ls -ld src and ls -la
    • grep -rn "pattern" src/index.ts
    • git diff package.json

Pre-Merge Checklist

  • Updated relevant documentation and README (if needed)
  • Added/updated tests (if needed)
  • Noted breaking changes (if any)
  • Validated on required platforms/methods:
    • MacOS
      • npm run
      • npx
      • Docker
      • Podman
      • Seatbelt
    • Windows
      • npm run
      • npx
      • Docker
    • Linux
      • npm run
      • npx
      • Docker

…ompound loops

Preserves shell variables during flag parsing in findUntrustedFlags,
exempts safe read-only/navigation flags (-C, --all, --grep, --oneline,
ls -ld, grep -rn), restricts argument matching to high-risk patterns,
and allows compound loops in PolicyEngine when subcommands match ALLOW rules.
Fixes #29579
Fixes #29650
…bstring flags

- Preserves case sensitivity for short flags in findUntrustedFlags so -C is not matched against high-risk lowercase -c
- Guards isHighRiskValSubstring so safe inspection flags (like --grep) taking URL/path substrings are not falsely flagged
- Strips leading newlines/whitespace in ShellTool Output metadata
- Adds unit test coverage for compound loops in PolicyEngine
@DavidAPierce
DavidAPierce requested a review from a team as a code owner October 7, 2026 19:51
@gemini-code-assist

Copy link
Copy Markdown
Contributor

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request addresses several false-positive security warnings encountered during shell command execution. By refining how shell variables are parsed, introducing a whitelist of safe POSIX flags, and improving the logic for evaluating compound commands, the changes ensure that legitimate, non-malicious operations are no longer blocked by the security policy engine. These updates enhance the usability of the CLI while maintaining security boundaries.

Highlights

  • Security Warning False Positives: Implemented a safe flag exemption list and improved shell variable handling to prevent harmless commands (like 'ls -ld' or 'git -C') from triggering untrusted security warnings.
  • Policy Engine Logic: Upgraded the PolicyEngine to allow compound commands and loops if all individual sub-commands match explicit ALLOW rules, preventing overly restrictive 'ASK_USER' blocks.
  • Context Tracking Refinement: Improved the untrusted context tracker by stripping internal shell metadata and refining argument matching to focus on high-risk patterns like URLs and absolute paths.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize the Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counterproductive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution. ↩

@github-actions github-actions Bot added the size/l A large sized PR label Oct 7, 2026
@github-actions

github-actions Bot commented Oct 7, 2026 •

Copy link
Copy Markdown

📊 PR Size: size/L

  • Lines changed: 859
  • Additions: +783
  • Deletions: -76
  • Files changed: 4

@gemini-code-assist gemini-code-assist Bot 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.

Code Review

This pull request enhances the policy engine to upgrade catch-all ASK_USER decisions to ALLOW for compound commands and loops when all decomposed sub-commands explicitly match ALLOW rules. It also refactors the untrusted context tracker to reduce false positives by introducing safe short/long flags and high-risk value flags, and preserving shell variables during parsing. The review feedback correctly identifies that checking rule?.decision === PolicyDecision.ALLOW for sub-commands and wrapped commands will fail to recognize sub-commands allowed by heuristics (which may have an undefined rule or a catch-all rule), and suggests checking the overall decision instead.

Comment thread packages/core/src/policy/policy-engine.ts
Comment thread packages/core/src/policy/policy-engine.ts
@github-actions

github-actions Bot commented Oct 7, 2026 •

Copy link
Copy Markdown

Size Change: +6.2 kB (+0.02%)

Total Size: 35.5 MB

Filename Size Change
./bundle/chunk-CXANBIRY.js 0 B -670 kB (removed) 🏆
./bundle/chunk-D3BOMMBH.js 0 B -3.77 kB (removed) 🏆
./bundle/chunk-F4CZAB4I.js 0 B -3.68 MB (removed) 🏆
./bundle/chunk-GSW6BRN4.js 0 B -13.7 kB (removed) 🏆
./bundle/chunk-KDW4U63A.js 0 B -16.8 MB (removed) 🏆
./bundle/chunk-QLEMV2UF.js 0 B -19.5 kB (removed) 🏆
./bundle/chunk-RSCAFT3A.js 0 B -49.2 kB (removed) 🏆
./bundle/chunk-YPRGXL2P.js 0 B -3.43 kB (removed) 🏆
./bundle/core-QZR2VNBJ.js 0 B -53 kB (removed) 🏆
./bundle/devtoolsService-C3ANIATO.js 0 B -147 kB (removed) 🏆
./bundle/gemini-GYYQAC77.js 0 B -649 kB (removed) 🏆
./bundle/interactiveCli-TLPRESQ7.js 0 B -1.31 MB (removed) 🏆
./bundle/liteRtServerManager-ZIA3M3VR.js 0 B -2.08 kB (removed) 🏆
./bundle/oauth2-provider-OGZLZA6B.js 0 B -10.6 kB (removed) 🏆
./bundle/chunk-6JIN3MBM.js 3.68 MB +3.68 MB (new file) 🆕
./bundle/chunk-7ZH2SBGC.js 16.8 MB +16.8 MB (new file) 🆕
./bundle/chunk-KUJT2EXI.js 3.77 kB +3.77 kB (new file) 🆕
./bundle/chunk-LHOPKHVS.js 49.2 kB +49.2 kB (new file) 🆕
./bundle/chunk-MC4SWB67.js 670 kB +670 kB (new file) 🆕
./bundle/chunk-QTUPJAST.js 3.43 kB +3.43 kB (new file) 🆕
./bundle/chunk-UKIRU37B.js 13.7 kB +13.7 kB (new file) 🆕
./bundle/chunk-WIUBHM3J.js 19.5 kB +19.5 kB (new file) 🆕
./bundle/core-7Y3HDYYJ.js 53 kB +53 kB (new file) 🆕
./bundle/devtoolsService-662ZZERF.js 147 kB +147 kB (new file) 🆕
./bundle/gemini-4S46B6HQ.js 649 kB +649 kB (new file) 🆕
./bundle/interactiveCli-NI7LRCCU.js 1.31 MB +1.31 MB (new file) 🆕
./bundle/liteRtServerManager-7VXXLHHX.js 2.08 kB +2.08 kB (new file) 🆕
./bundle/oauth2-provider-JISUJTOC.js 10.6 kB +10.6 kB (new file) 🆕
ℹ️ View Unchanged
Filename Size Change
./bundle/bundled/third_party/index.js 8 MB 0 B
./bundle/chunk-34MYV7JD.js 2.45 kB 0 B
./bundle/chunk-5AUYMPVF.js 858 B 0 B
./bundle/chunk-5PS3AYFU.js 1.18 kB 0 B
./bundle/chunk-664ZODQF.js 124 kB 0 B
./bundle/chunk-DAHVX5MI.js 206 kB 0 B
./bundle/chunk-IUUIT4SU.js 56.5 kB 0 B
./bundle/chunk-L5V3KIDT.js 1.62 kB 0 B
./bundle/chunk-L6PII3GR.js 41.5 kB 0 B
./bundle/cleanup-VSTME4QN.js 0 B -902 B (removed) 🏆
./bundle/devtools-TYCPOPV3.js 683 kB 0 B
./bundle/events-XB7DADIJ.js 418 B 0 B
./bundle/examples/hooks/scripts/on-start.js 188 B 0 B
./bundle/examples/mcp-server/example.js 1.43 kB 0 B
./bundle/gemini.js 5.09 kB 0 B
./bundle/getMachineId-bsd-TXG52NKR.js 1.55 kB 0 B
./bundle/getMachineId-darwin-7OE4DDZ6.js 1.55 kB 0 B
./bundle/getMachineId-linux-SHIFKOOX.js 1.34 kB 0 B
./bundle/getMachineId-unsupported-5U5DOEYY.js 1.06 kB 0 B
./bundle/getMachineId-win-6KLLGOI4.js 1.72 kB 0 B
./bundle/https-proxy-agent-JWL7GO47.js 479 B 0 B
./bundle/multipart-parser-E7RMVJWU.js 11.7 kB 0 B
./bundle/multipart-parser-KPBZEGQU.js 11.7 kB 0 B
./bundle/sandbox-macos-permissive-open.sb 8.69 kB 0 B
./bundle/sandbox-macos-permissive-proxied.sb 8.97 kB 0 B
./bundle/sandbox-macos-restrictive-open.sb 6.86 kB 0 B
./bundle/sandbox-macos-restrictive-proxied.sb 7.06 kB 0 B
./bundle/sandbox-macos-strict-open.sb 8.23 kB 0 B
./bundle/sandbox-macos-strict-proxied.sb 8.44 kB 0 B
./bundle/src-65GKNWUJ.js 45.4 kB 0 B
./bundle/src-U45KTUYT.js 45.7 kB 0 B
./bundle/src-XZYPU6PJ.js 352 kB 0 B
./bundle/start-JSIETQKJ.js 0 B -622 B (removed) 🏆
./bundle/tree-sitter-7U6MW5PS.js 274 kB 0 B
./bundle/tree-sitter-bash-34ZGLXVX.js 1.84 MB 0 B
./bundle/worker/worker-entry.js 363 kB 0 B
./bundle/cleanup-RWK2ANBO.js 902 B +902 B (new file) 🆕
./bundle/start-7ZD6REC3.js 622 B +622 B (new file) 🆕

compressed-size-action

@gemini-cli gemini-cli Bot added the area/security Issues related to security label Oct 7, 2026
Checking rule?.decision === PolicyDecision.ALLOW fails to recognize sub-commands
allowed by heuristics, checkers, or compound subshells where rule may be undefined
or point to a catch-all ASK_USER rule. Checking result.decision === ALLOW correctly
recognizes all allowed sub-commands when upgrading compound commands.

@kschaab kschaab 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.

Executive Summary & Verdict

This PR addresses false-positive security warnings on shell commands (#29579, #29650) across untrustedContextTracker and PolicyEngine.

Verdict: Request Changes

While the untrustedContextTracker updates effectively eliminate false positives on POSIX flags and shell loop variables, there are two blocking issues to address before merging:

  1. [PITFALL-008] / [OOP-CVA-002] (Security Regression in PolicyEngine.checkShellCommand): Because recursive sub-command checks in checkShellCommand pass skipHeuristics = true, the new bottom-up ASK_USER -> ALLOW upgrade reverses top-level applyShellHeuristics safety downgrades (dir_path escaping the workspace boundary, git commands in untrusted workspaces, or isDangerousCommand) whenever two commands are chained.
  2. [TEST-002] (Short-flag case sensitivity in extractUntrustedContext): While isHighRiskFlag now checks HIGH_RISK_VALUE_FLAGS case-sensitively for short flags, extractUntrustedContext still lowercases all tokens when populating untrustedTokens and Check 1A queries untrustedTokens using lowercased flag names.

Good PR Checkmarks Scorecard

ID Checkmark Grade Justification
[CHECK-1] PR Scope & Pure Refactor Isolation ✅ PASS Focused on eliminating false-positive shell confirmations across untrustedContextTracker and PolicyEngine for #29579 and #29650.
[CHECK-2] Pragmatic DRY (Don't Repeat Yourself) ⚠️ WARN Check 1A duplicates a narrower version of Check 1C's nextToken logic, and the normalized untrustedTexts substring scan is repeated three times in findUntrustedFlags.
[CHECK-3] Low Cyclomatic Complexity & Linear Flow ⚠️ WARN findUntrustedFlags has grown to 159 lines with 5 levels of nested conditionals inside the token loop.
[CHECK-4] OOP Class Hierarchy & CVA Fit ❌ FAIL checkShellCommand's bottom-up ASK_USER -> ALLOW upgrade conflicts with PolicyEngine's top-down applyShellHeuristics downgrade contract when skipHeuristics = true.
[CHECK-5] Self-Documenting Code Over Comments ⚠️ WARN Multi-clause boolean conditions in Check 1B/1C rely on section comments rather than small named predicates.
[CHECK-6] Comprehensive Test & Branch Coverage ❌ FAIL Missing negative tests for compound commands under heuristic downgrades (dir_path outside workspace / untrusted folder), and one new test mocks engine.check ([PITFALL-003]) to simulate a state unreachable when skipHeuristics = true.

Additional Note ([PR-HYGIENE-002])

  • PR Title: Please update the PR title from Fix/untrusted flags false positives to follow Conventional Commits per CONTRIBUTING.md (e.g., fix(core): eliminate false positives on untrusted command flags and compound loops).

Comment thread packages/core/src/policy/policy-engine.ts Outdated
Comment thread packages/core/src/policy/policy-engine.test.ts Outdated
Comment thread packages/core/src/utils/untrustedContextTracker.ts
Comment thread packages/core/src/utils/untrustedContextTracker.ts
Comment thread packages/core/src/utils/untrustedContextTracker.ts Outdated
@DavidAPierce DavidAPierce changed the title Fix/untrusted flags false positives fix(core): eliminate false positives on untrusted command flags and compound loops Oct 8, 2026
…ntextTracker

- Enforce safety heuristics on decomposed sub-commands and prevent compound command upgrades from reversing top-level heuristic safety downgrades
- Remove self-spy in PolicyEngine test and add negative tests for compound commands under heuristic downgrades
- Preserve case for short flags in extractUntrustedContext and add -E to safe flags
- Deduplicate nextToken and substring scanning in findUntrustedFlags
- Restrict variable matching to bare shell variables to detect env-prefixed script paths
…and upgrades

- In checkShellCommand, require decomposed sub-commands to match an explicit argsPattern ALLOW rule before upgrading catch-all ASK_USER to ALLOW
- Pass skipHeuristics=true during recursive sub-command checks so default heuristic allowances (e.g. isKnownSafeCommand) do not bypass user allowlists in chained commands
- Enforce workspace boundary, untrusted git, and dangerous command checks prior to compound upgrades
- Fixes test failures in shell-safety-regression.test.ts

@kschaab kschaab 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 initial review comments and tightening checkShellCommand in 03889af to require explicit argsPattern ALLOW rules on decomposed sub-commands!

All of the untrustedContextTracker fixes look great. There is one remaining issue in PolicyEngine.checkShellCommand (and its accompanying unit tests) around how isDangerous and isUntrustedGit are evaluated on compound commands—see inline comments below.

Comment thread packages/core/src/policy/policy-engine.ts
Comment thread packages/core/src/policy/policy-engine.test.ts

@kschaab kschaab 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.

LGTM! Thanks for addressing all the review feedback and adding thorough regression coverage.

@kschaab
kschaab added this pull request to the merge queue Oct 8, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Oct 8, 2026
@DavidAPierce
DavidAPierce enabled auto-merge October 8, 2026 20:35
@DavidAPierce
DavidAPierce added this pull request to the merge queue Oct 8, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Oct 8, 2026
@kschaab
kschaab added this pull request to the merge queue Oct 8, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Oct 8, 2026
@DavidAPierce
DavidAPierce added this pull request to the merge queue Oct 8, 2026
Merged via the queue into main with commit 2ce1a69 Oct 8, 2026
33 checks passed
@DavidAPierce
DavidAPierce deleted the fix/untrusted-flags-false-positives branch October 8, 2026 21:57
@DavidAPierce

Copy link
Copy Markdown
Contributor Author

/patch preview

@github-actions

github-actions Bot commented Oct 9, 2026

Copy link
Copy Markdown

🚀 [Step 1/4] Patch workflow(s) waiting for approval!

📋 Details:

  • Channels: preview
  • Commit: 2ce1a6963e9e53a04afaf76111e4527cfa7c5dd7
  • Workflows Created: 1

⏳ Status: The patch creation workflow has been triggered and is waiting for deployment approval. Please visit the specific workflow links below and approve the runs.

🔗 Track Progress:

@github-actions

github-actions Bot commented Oct 9, 2026

Copy link
Copy Markdown

🚀 [Step 2/4] Patch PR Created!

📋 Patch Details:

📝 Next Steps:

  1. Review and approve the hotfix PR: #29696
  2. Once merged, the patch release will automatically trigger
  3. You'll receive updates here when the release completes

🔗 Track Progress:

@github-actions

github-actions Bot commented Oct 9, 2026

Copy link
Copy Markdown

🚀 [Step 3/4] Patch Release Waiting for Approval!

📋 Release Details:

  • Environment: prod
  • Channel: preview → publishing to npm tag preview
  • Version: v0.64.0-preview.0
  • Hotfix PR: Merged ✅
  • Release Branch: release/v0.64.0-preview.0-pr-29672

⏳ Status: The patch release has been triggered and is waiting for deployment approval. Please visit the specific workflow run link below and approve the deployment. You'll receive another update when it completes.

🔗 Track Progress:

@github-actions

github-actions Bot commented Oct 9, 2026

Copy link
Copy Markdown

✅ [Step 4/4] Patch Release Complete!

📦 Release Details:

🎉 Status: Your patch has been successfully released and published to npm!

📝 What's Available:

🔗 Links:

@vonkad

vonkad commented Oct 12, 2026

Copy link
Copy Markdown

I just upgraded to the preview version and I still get the warnings.

@npu-sword

npu-sword commented Oct 19, 2026 •

Copy link
Copy Markdown

Are you just going to white list all command flags ?
So flag "-a" is safe but "-e" is a huge security risk ?
And flag "-C" is safe but "-c" is a huge security risk ?
This merge request is very weak and very disappointing to me.

Blackmanx pushed a commit to Blackmanx/gemini-cli that referenced this pull request Oct 20, 2026
…ompound loops (google-gemini#29672)

Co-authored-by: Keith Schaab <keith.schaab@gmail.com>
Bulibas added a commit to SinishaDjukic/worca-cc that referenced this pull request Oct 24, 2026
# Multi-harness: Gemini CLI and Qwen Code harnesses

Adds two run engines, both marked **beta**: `worca --engine gemini`
(Google's Gemini CLI) and `worca --engine qwen` (Qwen Code). Qwen Code
is a fork of Gemini CLI, so both run on one shared base, and each
per-engine file keeps only what really differs.

Unlike Cursor (#606), both adapters were **spiked against the real
binaries** (gemini 0.63.0, qwen 0.25.0). The captured streams, and every
fact the adapters rely on, are in `test/fixtures/gemini/README.md` and
`test/fixtures/qwen/README.md`.

## Usage

```bash
npm i -g @google/gemini-cli            # GEMINI_API_KEY in worca's env (or ~/.gemini/.env), or sign in once with `gemini`
npm i -g @qwen-code/qwen-code          # choose an auth type once with `qwen`, or set a whole provider's env
worca --project . --prompt "…" --engine gemini [--model gemini-3.8-flash] [--allow-unguarded-engine]
```

`WORCA_GEMINI_BIN` / `WORCA_QWEN_BIN` point worca at another binary.
Both engines are also offered on New pipeline, as a default engine in
Settings › Models, and in a project's Settings.

## Layout

- `src/core/engines/gemini-family.mjs` holds the shared parts:
- sessions named up front (`--session-id`) and resumed (`--resume`),
with one fresh retry when the CLI no longer knows the session
  - prompt on stdin
- workspace folders (`--include-directories`), refused inside Worca's
home as for Copilot
  - `--allowed-mcp-server-names`, so only the run's servers attach
  - MCP server shape, `${VAR}` refs kept
  - Claude tool names
  - error classes, preflight, the turn cap and the spawn runner
- `src/core/engines/gemini.mjs`: Gemini's own stream normalizer, its
policy TOML and the checkout `.gemini/settings.json`.
- `src/core/engines/qwen.mjs`: Qwen's stream is Claude-shaped, so it
goes through the existing `claude-events.mjs` normalizer after a small
rewrite: the session id gets its prefix, tool names become Claude's, and
the `tool_call` meta tool is unwrapped. Rules and MCP servers go into a
system settings file.
- `src/core/engines/project-files.mjs`: Cursor's checkout-file writer
and ownership ledger, moved out of `cursor.mjs` and given an engine
parameter. The harness now handles any engine's checkout files:
register, teardown, commit and diff.

## Capabilities (from the spike)

| | Gemini CLI | Qwen Code |
|---|---|---|
| resume, stream events, MCP tools, permission rules | yes | yes |
| system-prompt flag | **no**: `GEMINI_SYSTEM_MD` replaces the whole
built-in prompt, so the prompt is folded in | **yes**:
`--append-system-prompt`; a prompt too long for argv is folded into
stdin |
| cost | **no**: tokens only, shown as *cost unknown* | **no**: tokens
only |
| allowed tools, effort, sub-agents, hook telemetry, skills, sub-agent
system prompt, turn budget | no | no (`--max-session-turns` did not cap
tool calls in the spike) |

## Guardrails

| Rule | Gemini CLI (`--admin-policy` TOML, admin tier) | Qwen Code
(Claude syntax, as written) |
|---|---|---|
| bare `Bash`, `WebFetch`, `WebSearch`, MCP server or tool | held | held
|
| `Bash(git push:*)` | **partial**: prefix match; the spike showed `env
git log` passes | **partial**: same gap |
| `Read(…)` | **partial**: argsPattern on the file tools; the shell is
not covered | held (a Read rule also stopped `cat` in the spike) |
| `Edit(…)`, `Write(…)`, `NotebookEdit(…)` | **partial**: same |
**partial**: the file tools stop, but the YOLO shell still writes the
path |
| unknown tools | unenforced | unenforced |

- **Gemini ignores a policy file it cannot parse.** It only prints
"Policy file error" on stderr and runs with no rule. The adapter stops
the child on that line (a new `onStderrLine` hook in `superviseSpawn`).
- **Qwen's env under scrub:** with a scrubbing set, Qwen gets `QWEN_`
names plus only the provider it is set to (its chosen sign-in, else the
provider its env names): `OPENAI_`/`DASHSCOPE_`, `GEMINI_`, `GOOGLE_` or
`ANTHROPIC_`. worca's own keys for other providers stay out of its
shell.
- An `--admin-policy` file is also skipped when the system policy folder
holds `.toml` files, so the run refuses then.

## Checkout files and MCP

- **Gemini** reads MCP servers only from settings files, and its
system-settings path must be owned by root. So worca writes the
checkout's `.gemini/settings.json`, with the same ownership,
exclude-line and teardown handling as Cursor's files. A tracked
`.gemini/settings.json` makes a run with MCP servers refuse.
- Gemini needs `GEMINI_CLI_TRUST_WORKSPACE=true`: without it a headless
run exits 55, and `--skip-trust` drops the checkout's MCP servers.
- **Qwen** honours `QWEN_CODE_SYSTEM_SETTINGS_PATH` for any owner. worca
writes its servers and deny rules there, in its temp folder, merged over
the machine's own system settings. Nothing goes into the checkout.
(`--mcp-config` does not fill `${VAR}`, so it is not used.)

## Elsewhere

- **Helper jobs** (titles, overview, PR description, Auto classifier,
night decider) run on Claude, as on Cursor. The new `SIGN_IN_ENGINES`
(cursor, gemini, qwen) replaces the `engine === 'cursor'` checks for:
  - model fields: no env, no upstream, no effort
  - helper routing
  - the Test button's scratch cwd and signed-out hint
  - Ask proposals and the models editor
- **Read-only spawns and Ask refuse** on both engines. Both CLIs have an
`--approval-mode plan`, but no lockdown was verified, so read-only jobs
and Ask stay on Claude.
- **Models:** worca ships none. Users add catalog models with Engine
Gemini CLI or Qwen Code.
- **UI:** engine picker buttons with the Beta badge, Settings engine
cards, the models editor (connection text, no effort or pricing), and
the usage-limit resume offers.
- **Docs:** a new "Gemini CLI and Qwen Code" section in
`docs/models.md`.

## Tests

- New test files:
- `test/gemini-adapter.test.mjs` and `test/qwen-adapter.test.mjs`: the
normalizers against the real captures, argv, the rule plans, preflight,
and the spawn paths with fake binaries
(`test/helpers/fake-gemini-family.mjs`)
  - `test/gemini-qwen-helpers.test.mjs`
- Extended: registry, beta, readiness, switch, catalog, manifest,
policy, cascade, gate (`.gemini/settings.json` teardown and commit),
picker, settings and models-view tests.

```bash
node tools/test.mjs test/gemini-adapter.test.mjs test/qwen-adapter.test.mjs test/gemini-qwen-helpers.test.mjs
```

**Status: draft.**

**End-to-end runs.** Real worca pipelines (`--yes`, default workflow,
Permissive set) ran on both real CLIs, with a scratch `HOME` and
`WORCA_HOME` and a small task: add `add(a, b)` to `math.mjs` with a
`node:test` test.
- **Gemini CLI** (`gemini-3.8-flash` via `auto`): two runs. Both
completed all 7 executions, the reviewer passed, and the result was
committed on the run branch; the summary says *cost unknown*.
- **Qwen Code** (signed in through Gemini: `GEMINI_API_KEY` +
`GEMINI_MODEL`): one run. It completed the same way; the summary says
*cost unknown*.
- **Rerun on the rebased commit (2026-10-20):** gemini 0.64.0 and qwen
0.25.3 (the current stable releases), with the Normal set and
`--allow-unguarded-engine`, the same task, and a scratch `HOME` and
`WORCA_HOME`.
- **Gemini CLI** (model `auto`): 7 executions in 5m35s. Refine and the
reviewer were clean, the result was committed, and `npm test` passes on
it (2/2). No "denied by policy" this time.
- **Qwen Code** (`GEMINI_API_KEY` + `GEMINI_MODEL=gemini-3.8-flash`): 7
executions in 4m03s. Same outcome, `npm test` 8/8.
- The only tool errors were reads of files that did not exist yet, and
one `npm test` run before the test file was written.
- The first Gemini run found three bugs, now fixed:
- The implementer could not read its plan: Gemini's file tools read only
the workspace, and plans live in worca's store. Each store folder now
brings its project's store folder into the workspace.
- Gemini passes cwd-relative paths, so the run log showed them under
worca's own directory. Tool paths are now made absolute against the
spawn's cwd.
- The CLI's step lines printed `$0.00` on engines that report no cost.
This affected Copilot and Cursor too. They now show no cost, matching
the run total.
- **Known Gemini behaviour:** inside worca runs, Gemini now and then
answers a plain `node --test math.test.mjs` with "Tool execution denied
by policy".
- worca passed no policy file on those runs (the spawn argv was
checked).
- The same command ran fine outside worca, and the agent recovered every
time (it re-ran the test as `./math.test.mjs`).
- **Likely cause:** a taint check in Gemini CLI itself, added in 0.63
([#29250](google-gemini/gemini-cli#29250)). In a
non-interactive run it turns an allowed shell call into a deny, with no
rule named, in two cases:
- a flag or path in the command appeared earlier in tool output (shell,
MCP or web output). For example, `--test` printed by `cat package.json`
taints a later `node --test`.
- the session edited a build file and then runs a build or test command
(npm, npx, make, pytest and others).
- It runs before YOLO and has no setting to turn it off. 0.64
([#29672](google-gemini/gemini-cli#29672))
narrows it, but `--test` is not on its safe list. Two open Gemini PRs
(#29833, #29719) would narrow it further.
  - worca cannot switch it off, so it is left as a known limitation.

**Tests.**
- Full suite: 9,050 tests, 15 failures on the first run.
- 9 were tests that pin the engine lists and the resume offers. They now
include Gemini CLI and Qwen Code.
- 4 were perf/timing tests that failed under the parallel run's load.
They pass when run alone.
- `wsmap-deps` fails locally only because this worktree has no
`node_modules`. CI installs them.
- My real `~/.worca-cc/settings.json` was unchanged before and after.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/security Issues related to security size/l A large sized PR

Projects

None yet

4 participants