口岸的关员从不靠记性,也从不靠脾气。她面前压着一部厚厚的《规章汇编》,每一条都盖着两枚印:一枚是优先级的数字,一枚是签发机关——总署、口岸、保税区、外包机构、默认章程,五级由高到低。
每来一票货,她不逐条琢磨,而是把整部汇编按优先级从高到低一路翻下去,头一条对得上的,就照它落章:或"放行",或"扣验"(按铃请上级来当面看一眼),或"退运"。翻到第一条命中就盖章,后面的再合理也不再看。
一票"只是过来照个相"的样品(只读),汇编第 50 条早写着"一律放行",她眼皮都不抬。一票要动仓库存货的(写文件),命中的是第 10 条"须叫人当面点头",于是她按铃。
有意思的是所谓"免检期"。旺季挂出"绿色通道"全部放行,并不是关员一时心软——那是汇编里一条实实在在印着的规章:"凡此期内,除另有规定,一切放行",优先级 998。可同一页上还钉着一条优先级 999 的铁规:"须当面确认者,免检期内照样确认。"数字更大,压得住它。放不放、问不问,全是白纸黑字排好的次序,不是谁的临场发挥。
再有意思的是"规矩本身分层"。同一票货,总署的批文能压过口岸的、口岸的能压过外包的——不是靠吵架,是每条规章的优先级里就带着它的出身:默认章程落在第一层,用户改的落在第四层,数字一算,高层自动盖过低层。
哪天关长对某家的货挥挥手"这家的,以后都放",文书不会去改关员的性子,而是往汇编里添一条更窄的新规章,盖上优先级、归进"用户"这一层——下次这家的货自动命中放行。
这就是权限闸门:把"能不能动手"整个写成一部按优先级排序、分级签发的规章,最高命中者裁决 allow / ask / deny——连"全部放行"都只是其中一行数据,而非一段写死的代码。
先猜揭晓前,先押一个猜测 · 你觉得这讲的是?
谜底 · The Concept
Gemini CLI — the permission gate as a priority-sorted TOML policy engine: ALLOW / ASK_USER / DENY decided by data, where even YOLO is a rule
Other Coding Agents · 他山之石
中文速览 · Quick read
Citations pinned at gemini-cli@b31b755 (cloned 2026-07-08).
Gemini CLI is Google's terminal coding agent — a TypeScript monorepo shipping as package version 0.51.0-nightly (gemini-cli/package.json:3). The gemini binary resolves to dist/index.js (gemini-cli/packages/cli/package.json:11-13), whose main() lives at gemini-cli/packages/cli/src/gemini.tsx:350. The split that defines the codebase is UI vs harness: the Ink/React terminal app and the non-interactive runner live in packages/cli/src/, while the entire engine lives in packages/core/src/ — the loop in core/, tool definitions in tools/, dispatch in scheduler/, the permission gate in policy/, context in context/, and sub-agents in agents/. Two more organs sit alongside the five here — a hook system (a BeforeTool check runs in the scheduler at gemini-cli/packages/core/src/scheduler/scheduler.ts:609-615) and a skills/ directory — but the personality of Gemini CLI shows up most in one place: its permission gate is not code, it is priority-sorted TOML data.
The loop is split across two layers. The outer loop lives in the driver. In non-interactive mode it is a literal while (true) (gemini-cli/packages/cli/src/nonInteractiveCli.ts:310-324) that calls geminiClient.sendMessageStream(...), collects every ToolCallRequest event from the stream into a batch, hands the batch to the scheduler, and then feeds the tool results back as the next user message — currentMessages = [{ role: 'user', parts: toolResponseParts }] (gemini-cli/packages/cli/src/nonInteractiveCli.ts:560) — before looping. The interactive Ink UI does the same through React hooks: handleCompletedTools re-submits the responses with { isContinuation: true } (gemini-cli/packages/cli/src/ui/hooks/useGeminiStream.ts:2019-2055).
The inner engine is GeminiClient.sendMessageStream, a recursive async generator (gemini-cli/packages/core/src/core/client.ts:910-917) bounded by MAX_TURNS = 100 (gemini-cli/packages/core/src/core/client.ts:79). It delegates to processTurn (gemini-cli/packages/core/src/core/client.ts:614), which builds a Turn whose run translates raw model chunks into typed Thought / Content / ToolCallRequest events (gemini-cli/packages/core/src/core/turn.ts:257-279). Two loop-control services run inside the engine. A LoopDetectionService inspects every event for runaway repetition (gemini-cli/packages/core/src/core/client.ts:747-763). And the inverse guard: when a turn ends with no pending tool calls, an LLM-based checkNextSpeaker can decide the model should keep going, recursing with a synthetic "Please continue." user message (gemini-cli/packages/core/src/core/client.ts:880-904). That self-continuation is the one loop organ Claude Code has no equivalent for — a model-judged decision to iterate even when nothing asked it to.
A tool is a DeclarativeTool carrying name, displayName, description, kind, and a JSON parameterSchema (gemini-cli/packages/core/src/tools/tools.ts:462-478). The load-bearing design is build-then-execute: BaseDeclarativeTool.build(params) validates the model's raw args against the schema and returns a frozen ToolInvocation — a first-class object binding the validated params to getDescription(), shouldConfirmExecute(), and execute() (gemini-cli/packages/core/src/tools/tools.ts:683-694, gemini-cli/packages/core/src/tools/tools.ts:47-107). Tools register in a ToolRegistry (gemini-cli/packages/core/src/tools/tool-registry.ts:271). Dispatch is a dedicated state machine, CoreToolScheduler: schedule(requests, signal) (gemini-cli/packages/core/src/scheduler/scheduler.ts:195-223) turns each request into a Validating call, then _processQueue walks it through (gemini-cli/packages/core/src/scheduler/scheduler.ts:428-433). Whether calls run in parallel is decided by the model itself, through a wait_for_previous boolean the harness injects into every tool's schema (gemini-cli/packages/core/src/tools/tools.ts:538-565, honored at gemini-cli/packages/core/src/scheduler/scheduler.ts:552-569).
The gate is where Gemini CLI is most distinctive. Every decision is one of ALLOW | DENY | ASK_USER (gemini-cli/packages/core/src/policy/types.ts:10-14), and the whole engine is a list of rules sorted once by priority (gemini-cli/packages/core/src/policy/policy-engine.ts:209-212), scanned top-down until the first matching rule wins (gemini-cli/packages/core/src/policy/policy-engine.ts:577-632). Rules come from TOML files layered in tiers — Admin > User > Workspace > Extension > Default, encoded as a priority band tier + priority/1000 (gemini-cli/packages/core/src/policy/policies/plan.toml:6-14) — and each rule can be scoped by tool name (wildcards), MCP server, args regex, subagent, interactivity, and approval mode (ruleMatches, gemini-cli/packages/core/src/policy/policy-engine.ts:85-196). The four approval modes DEFAULT | AUTO_EDIT | YOLO | PLAN (gemini-cli/packages/core/src/policy/types.ts:48-53) are ordered by permissiveness (gemini-cli/packages/core/src/policy/types.ts:60-65), but they are not if-branches — they are just a filter column on rules. Read-only tools are allowed at priority 50 (gemini-cli/packages/core/src/policy/policies/read-only.toml:30-56); write and shell tools ask at priority 10 (gemini-cli/packages/core/src/policy/policies/write.toml:47-57); AUTO_EDIT is a priority-15 override that flips write_file/replace to allow (gemini-cli/packages/core/src/policy/policies/write.toml:36-40, gemini-cli/packages/core/src/policy/policies/write.toml:65-69). YOLO is not an if-statement but a rule: toolName = "*" / decision = "allow" / priority = 998 / modes = ["yolo"] (gemini-cli/packages/core/src/policy/policies/yolo.toml:50-56) — and the priority-999 ask_user rule outranks it, so YOLO still asks when the model needs a human decision (gemini-cli/packages/core/src/policy/policies/yolo.toml:33-38). PLAN mode is a priority-40 deny-all with narrower rows stacked above it — an allow for two investigator agents at 50, a sharper write-tool deny at 65 (gemini-cli/packages/core/src/policy/policies/plan.toml:76-81, gemini-cli/packages/core/src/policy/policies/plan.toml:97-103, gemini-cli/packages/core/src/policy/policies/plan.toml:131-136). When no rule matches, the default is ASK_USER interactively and DENY non-interactively (gemini-cli/packages/core/src/policy/policy-engine.ts:253-256).
The scheduler consumes the verdict between validation and execution. After the BeforeTool hook (gemini-cli/packages/core/src/scheduler/scheduler.ts:609-615), checkPolicy asks the engine (gemini-cli/packages/core/src/scheduler/policy.ts:53-72, wired at gemini-cli/packages/core/src/scheduler/scheduler.ts:639-648): DENY becomes an error result without executing (gemini-cli/packages/core/src/scheduler/scheduler.ts:650-666), and ASK_USER enters resolveConfirmation, which flips the call to awaiting_approval and blocks on a message-bus reply (gemini-cli/packages/core/src/scheduler/scheduler.ts:672-686). The best detail: an "always allow" answer is persisted not into a set but by narrowing and writing a new policy rule (updatePolicy, gemini-cli/packages/core/src/scheduler/scheduler.ts:691-700) — restricted to the current approval mode and anything more permissive (gemini-cli/packages/core/src/scheduler/policy.ts:133-145). The ordinary "Allow for this session" click (ProceedAlways, gemini-cli/packages/cli/src/ui/components/messages/ToolConfirmationMessage.tsx:295-297) never sets a persistScope — only the separate save-to-settings outcome, ProceedAlwaysAndSave, does that, and only when the folder isn't a trusted workspace with its own policies dir (gemini-cli/packages/core/src/scheduler/policy.ts:147-157, forwarded unchanged at gemini-cli/packages/core/src/scheduler/policy.ts:216-238). So createPolicyUpdater defaults an unset scope to Workspace, not User (gemini-cli/packages/core/src/policy/config.ts:728-731,770-773) — landing at priority fraction 950 (gemini-cli/packages/core/src/policy/types.ts:370-377) on tier 3 (gemini-cli/packages/core/src/policy/config.ts:83-86), i.e. effective 3.950 — still enough to outrank every default-tier rule next time. Only a saved, untrusted-folder "always allow" climbs to the User tier, 4.950.
Delegation is a real organ that flows through this same gate. AgentTool is published to the model as invoke_agent (gemini-cli/packages/core/src/tools/tool-names.ts:191) with params { agent_name, prompt } (gemini-cli/packages/core/src/agents/agent-tool.ts:43-73); built-in agents are registered in code (gemini-cli/packages/core/src/agents/registry.ts:284-287) and user agents load from Markdown-with-YAML-frontmatter files (gemini-cli/packages/core/src/agents/agentLoader.ts:322-356). LocalAgentExecutor (gemini-cli/packages/core/src/agents/local-executor.ts:120) runs the child in its own while (true) loop (gemini-cli/packages/core/src/agents/local-executor.ts:674-688) with a separate chat and a restricted tool list, and termination is protocol-based — the child must call complete_task or it is an ERROR_NO_COMPLETE_TASK_CALL (gemini-cli/packages/core/src/agents/local-executor.ts:362-371). Sub-agents are allowed by default (invoke_agent is a priority-50 allow in gemini-cli/packages/core/src/policy/policies/agents.toml:1-10), and the engine injects the subagent's name as a virtual tool alias so a policy can target one agent by name (gemini-cli/packages/core/src/policy/policy-engine.ts:562-570).
Every turn opens with a context check inside processTurn. Two systems coexist behind getContextManagementConfig().enabled (gemini-cli/packages/core/src/core/client.ts:643): a newer graph-based ContextManager pipeline and the classic tryCompressChat path. After either, the client computes the remaining budget as tokenLimit(model) - lastPromptTokenCount and refuses to send a request that would overflow, yielding ContextWindowWillOverflow (gemini-cli/packages/core/src/core/client.ts:696-715). Compression proper lives in ChatCompressionService.compress (gemini-cli/packages/core/src/context/chatCompressionService.ts:271-285): it no-ops until history exceeds 50% of the model's token limit (DEFAULT_COMPRESSION_TOKEN_THRESHOLD = 0.5 gemini-cli/packages/core/src/context/chatCompressionService.ts:41), then keeps the most recent ~30% (COMPRESSION_PRESERVE_THRESHOLD = 0.3 gemini-cli/packages/core/src/context/chatCompressionService.ts:47) and sends the older portion to a summarizer LLM that must produce a <state_snapshot> — with an explicit anchor instruction to merge any previous snapshot rather than lose it (gemini-cli/packages/core/src/context/chatCompressionService.ts:353-374).
Durable memory is GEMINI.md (DEFAULT_CONTEXT_FILENAME, gemini-cli/packages/core/src/tools/memoryTool.ts:11-16) — the direct analogue of CLAUDE.md. Discovery traverses upward from the cwd collecting GEMINI.md variants (gemini-cli/packages/core/src/utils/memoryDiscovery.ts:458-470), and the result is a HierarchicalMemory { global, extension, project, userProjectMemory } (gemini-cli/packages/core/src/config/memory.ts:7-13) flattened into labeled sections and passed into the system prompt via getCoreSystemPrompt(config, userMemory, ...) (gemini-cli/packages/core/src/core/prompts.ts:23-35).
Same five organs, but the gate is rehoused from code into data. Each row maps Gemini CLI's mechanism to the Claude Code page that dissects the equivalent:
- The loop → /stories/loop/1/ and /stories/loop/2/. Same "call model → run tools → feed results back as a user message → loop" cycle (gemini-cli/packages/cli/src/nonInteractiveCli.ts:560), with the same
MAX_TURNS-style bound and loop detector — plus one extra organ: an LLMcheckNextSpeakerthat continues the loop even without tool calls (gemini-cli/packages/core/src/core/client.ts:880-904). - Tool anatomy → /stories/tools/1/. Same name + schema + validate + execute, but Gemini CLI reifies a validated call as a first-class frozen
ToolInvocation(gemini-cli/packages/core/src/tools/tools.ts:47-107) and runs dispatch through an explicit multi-status scheduler (gemini-cli/packages/core/src/scheduler/scheduler.ts:428-433) rather than executing inline. - The permission gate → /stories/tools/2/. Same three-way
allow / ask / denyverdict and same "ask → remember it" pattern, but where Claude Code's checks are code branches, Gemini CLI's are priority-sorted TOML data (gemini-cli/packages/core/src/policy/policy-engine.ts:209-212), even the approval modes are just rule filters (gemini-cli/packages/core/src/policy/types.ts:48-53), and "always allow" persists as a written rule (gemini-cli/packages/core/src/scheduler/scheduler.ts:691-700). - Compaction → /stories/context/1/. Threshold-triggered LLM summarization that preserves a recent tail, with a Gemini-specific twist: a mergeable
<state_snapshot>anchor so a second compaction folds the first in rather than dropping it (gemini-cli/packages/core/src/context/chatCompressionService.ts:353-374). - Memory → /stories/context/2/.
GEMINI.mdis the direct analogue ofCLAUDE.md, discovered hierarchically upward and injected into the system prompt (gemini-cli/packages/core/src/utils/memoryDiscovery.ts:458-470, gemini-cli/packages/core/src/core/prompts.ts:23-35). - Sub-agents → /stories/multiagent/1/. Same shape — a tool spawns a child loop with separate history and a restricted tool set, defined by Markdown frontmatter (gemini-cli/packages/core/src/agents/agentLoader.ts:322-356) — but with a hard completion protocol: the child must call
complete_taskor it errors (gemini-cli/packages/core/src/agents/local-executor.ts:362-371).
-
Permissions as priority-sorted TOML data. The entire gate — YOLO, plan mode, per-mode transitions, MCP trust, "always allow" persistence — is declarative rules with numeric priorities in tiered bands (gemini-cli/packages/core/src/policy/policies/plan.toml:6-14, gemini-cli/packages/core/src/policy/policy-engine.ts:209-212). "Allow everything in YOLO" is a single data row, not a code path (gemini-cli/packages/core/src/policy/policies/yolo.toml:50-56), and a user's "always allow" click writes a narrowed rule back into the engine (gemini-cli/packages/core/src/scheduler/scheduler.ts:691-700). The gate becomes auditable, overridable, and diffable.
-
Model-controlled parallelism (
wait_for_previous). The harness injects await_for_previous: booleaninto every tool's JSON schema, so the model itself declares data dependencies between calls in a batch (gemini-cli/packages/core/src/tools/tools.ts:538-565); the scheduler batches contiguous parallelizable calls, forcing edit tools sequential regardless (gemini-cli/packages/core/src/scheduler/scheduler.ts:552-569). Parallelism becomes a schema-level contract instead of a harness-side guess. -
The inverse loop guard,
checkNextSpeaker. Beside the runaway detector, an LLM judge decides whether the model's last turn implies it should keep going, driving a synthetic"Please continue."continuation (gemini-cli/packages/core/src/core/client.ts:880-904). Detection and continuation are both first-class, model-assisted harness services rather than fixed stop conditions.
classDiagram
class UI {
<<packages/cli · surfaces>>
+Ink React terminal app
+non-interactive while true driver
+feed tool results back as user message
}
class Engine {
<<packages/core · the harness>>
+GeminiClient recursive generator
+MAX_TURNS 100
}
class TurnLoop {
<<organ · loop>>
+Turn.run streams typed events
+LoopDetectionService watches events
+checkNextSpeaker continues without tool calls
}
class Scheduler {
<<organ · tools + dispatch>>
+DeclarativeTool build then execute
+ToolInvocation frozen validated call
+status machine validating to success
+wait_for_previous model picks parallelism
}
class PolicyEngine {
<<organ · the gate = DATA>>
+rules sorted once by priority
+first matching rule wins
+modes are a filter column not code
+YOLO is priority 998 allow row
+always allow writes a new user rule
}
class Context {
<<organ · context + memory>>
+compress over 50 percent keep last 30
+mergeable state_snapshot anchor
+GEMINI.md discovered upward
}
class SubAgents {
<<organ · sub-agents>>
+invoke_agent tool spawns a child loop
+separate chat restricted tools
+must call complete_task or error
}
UI --> Engine : sendMessageStream
Engine *-- TurnLoop : processTurn
TurnLoop --> Scheduler : batch of ToolCallRequest
Scheduler --> PolicyEngine : checkPolicy between validate and execute
PolicyEngine ..> Scheduler : ALLOW or ASK_USER or DENY
Engine --> Context : check budget each turn
Scheduler --> SubAgents : invoke_agent
SubAgents ..> PolicyEngine : child calls pass the same gate 读法:Gemini CLI 把 UI(Ink 终端 + 非交互 while (true) 驱动)
和 harness(packages/core)分开;引擎是一个受 MAX_TURNS=100 约束的递归生成器
(core/client.ts:910-917、:79)。五个器官:
循环(Turn.run 流式产出类型化事件,外加一条 Claude Code 没有的
checkNextSpeaker——没有工具调用也能续跑,core/client.ts:880-904)、
工具与派发(build 出冻结的 ToolInvocation,状态机走 validating→…→success,
tools.ts:683-694、scheduler/scheduler.ts:428-433)、
权限门——本页主角:一部按优先级排序、逐条匹配、头一条命中即裁决的 TOML 规章,
modes 只是过滤器列、YOLO 只是优先级 998 的一行数据
(policy/policy-engine.ts:209-212、policy/policies/yolo.toml:50-56)、
上下文与记忆(超 50% 压缩、保留最近 30%、可合并的 <state_snapshot>、
向上发现的 GEMINI.md,context/chatCompressionService.ts:271-285、
tools/memoryTool.ts:11-16)、
子智能体(invoke_agent 开一个必须 complete_task 收尾的子循环,
agents/local-executor.ts:362-371)。
实线 Scheduler --> PolicyEngine 就是验证与执行之间那道闸口
(scheduler/scheduler.ts:639-648)。
Gemini CLI 的权限门不是代码,是一部 按优先级排序的 TOML 规章。同一串工具调用, 换一个审批模式或交互性,命中的规则就变——每条调用都亮出哪条规则以最高优先级胜出、 裁决 allow / ask_user / deny。开关任一规则,看同一调用改判。
这正是 策略引擎:规则先按优先级排一次序,再从高往低扫,头一条命中者裁决
(policy-engine.ts:209-212, :577-632);审批模式只是规则上的一列过滤器
(modes,types.ts:48-53)。YOLO 不是 if,是优先级 998 的一行
toolName="*" / decision="allow"(yolo.toml:50-56);同文件里优先级 999 的
ask_user 规则照样压过它(yolo.toml:33-38)。都不命中就走默认:交互 ask_user、
无头 deny(policy-engine.ts:253-256)。点『总是允许』,答复被收窄成一条新规则
写回 workspace 层(scheduler.ts:691-700)——不是塞进内存集合。
注:run_shell_command 命中规则后还要再过一层命令解析启发式(policy-engine.ts 的 shell heuristics),此处只演示策略规则那一步的裁决。
The lay of the land
Citations pinned at gemini-cli@b31b755 (cloned 2026-07-08).
Gemini CLI is Google's terminal coding agent — a TypeScript monorepo shipping as package version 0.51.0-nightly (gemini-cli/package.json:3). The gemini binary resolves to dist/index.js (gemini-cli/packages/cli/package.json:11-13), whose main() lives at gemini-cli/packages/cli/src/gemini.tsx:350. The split that defines the codebase is UI vs harness: the Ink/React terminal app and the non-interactive runner live in packages/cli/src/, while the entire engine lives in packages/core/src/ — the loop in core/, tool definitions in tools/, dispatch in scheduler/, the permission gate in policy/, context in context/, and sub-agents in agents/. Two more organs sit alongside the five here — a hook system (a BeforeTool check runs in the scheduler at gemini-cli/packages/core/src/scheduler/scheduler.ts:609-615) and a skills/ directory — but the personality of Gemini CLI shows up most in one place: its permission gate is not code, it is priority-sorted TOML data.
The loop
The loop is split across two layers. The outer loop lives in the driver. In non-interactive mode it is a literal while (true) (gemini-cli/packages/cli/src/nonInteractiveCli.ts:310-324) that calls geminiClient.sendMessageStream(...), collects every ToolCallRequest event from the stream into a batch, hands the batch to the scheduler, and then feeds the tool results back as the next user message — currentMessages = [{ role: 'user', parts: toolResponseParts }] (gemini-cli/packages/cli/src/nonInteractiveCli.ts:560) — before looping. The interactive Ink UI does the same through React hooks: handleCompletedTools re-submits the responses with { isContinuation: true } (gemini-cli/packages/cli/src/ui/hooks/useGeminiStream.ts:2019-2055).
The inner engine is GeminiClient.sendMessageStream, a recursive async generator (gemini-cli/packages/core/src/core/client.ts:910-917) bounded by MAX_TURNS = 100 (gemini-cli/packages/core/src/core/client.ts:79). It delegates to processTurn (gemini-cli/packages/core/src/core/client.ts:614), which builds a Turn whose run translates raw model chunks into typed Thought / Content / ToolCallRequest events (gemini-cli/packages/core/src/core/turn.ts:257-279). Two loop-control services run inside the engine. A LoopDetectionService inspects every event for runaway repetition (gemini-cli/packages/core/src/core/client.ts:747-763). And the inverse guard: when a turn ends with no pending tool calls, an LLM-based checkNextSpeaker can decide the model should keep going, recursing with a synthetic "Please continue." user message (gemini-cli/packages/core/src/core/client.ts:880-904). That self-continuation is the one loop organ Claude Code has no equivalent for — a model-judged decision to iterate even when nothing asked it to.
Tools and the gate
A tool is a DeclarativeTool carrying name, displayName, description, kind, and a JSON parameterSchema (gemini-cli/packages/core/src/tools/tools.ts:462-478). The load-bearing design is build-then-execute: BaseDeclarativeTool.build(params) validates the model's raw args against the schema and returns a frozen ToolInvocation — a first-class object binding the validated params to getDescription(), shouldConfirmExecute(), and execute() (gemini-cli/packages/core/src/tools/tools.ts:683-694, gemini-cli/packages/core/src/tools/tools.ts:47-107). Tools register in a ToolRegistry (gemini-cli/packages/core/src/tools/tool-registry.ts:271). Dispatch is a dedicated state machine, CoreToolScheduler: schedule(requests, signal) (gemini-cli/packages/core/src/scheduler/scheduler.ts:195-223) turns each request into a Validating call, then _processQueue walks it through (gemini-cli/packages/core/src/scheduler/scheduler.ts:428-433). Whether calls run in parallel is decided by the model itself, through a wait_for_previous boolean the harness injects into every tool's schema (gemini-cli/packages/core/src/tools/tools.ts:538-565, honored at gemini-cli/packages/core/src/scheduler/scheduler.ts:552-569).
The gate is where Gemini CLI is most distinctive. Every decision is one of ALLOW | DENY | ASK_USER (gemini-cli/packages/core/src/policy/types.ts:10-14), and the whole engine is a list of rules sorted once by priority (gemini-cli/packages/core/src/policy/policy-engine.ts:209-212), scanned top-down until the first matching rule wins (gemini-cli/packages/core/src/policy/policy-engine.ts:577-632). Rules come from TOML files layered in tiers — Admin > User > Workspace > Extension > Default, encoded as a priority band tier + priority/1000 (gemini-cli/packages/core/src/policy/policies/plan.toml:6-14) — and each rule can be scoped by tool name (wildcards), MCP server, args regex, subagent, interactivity, and approval mode (ruleMatches, gemini-cli/packages/core/src/policy/policy-engine.ts:85-196). The four approval modes DEFAULT | AUTO_EDIT | YOLO | PLAN (gemini-cli/packages/core/src/policy/types.ts:48-53) are ordered by permissiveness (gemini-cli/packages/core/src/policy/types.ts:60-65), but they are not if-branches — they are just a filter column on rules. Read-only tools are allowed at priority 50 (gemini-cli/packages/core/src/policy/policies/read-only.toml:30-56); write and shell tools ask at priority 10 (gemini-cli/packages/core/src/policy/policies/write.toml:47-57); AUTO_EDIT is a priority-15 override that flips write_file/replace to allow (gemini-cli/packages/core/src/policy/policies/write.toml:36-40, gemini-cli/packages/core/src/policy/policies/write.toml:65-69). YOLO is not an if-statement but a rule: toolName = "*" / decision = "allow" / priority = 998 / modes = ["yolo"] (gemini-cli/packages/core/src/policy/policies/yolo.toml:50-56) — and the priority-999 ask_user rule outranks it, so YOLO still asks when the model needs a human decision (gemini-cli/packages/core/src/policy/policies/yolo.toml:33-38). PLAN mode is a priority-40 deny-all with narrower rows stacked above it — an allow for two investigator agents at 50, a sharper write-tool deny at 65 (gemini-cli/packages/core/src/policy/policies/plan.toml:76-81, gemini-cli/packages/core/src/policy/policies/plan.toml:97-103, gemini-cli/packages/core/src/policy/policies/plan.toml:131-136). When no rule matches, the default is ASK_USER interactively and DENY non-interactively (gemini-cli/packages/core/src/policy/policy-engine.ts:253-256).
The scheduler consumes the verdict between validation and execution. After the BeforeTool hook (gemini-cli/packages/core/src/scheduler/scheduler.ts:609-615), checkPolicy asks the engine (gemini-cli/packages/core/src/scheduler/policy.ts:53-72, wired at gemini-cli/packages/core/src/scheduler/scheduler.ts:639-648): DENY becomes an error result without executing (gemini-cli/packages/core/src/scheduler/scheduler.ts:650-666), and ASK_USER enters resolveConfirmation, which flips the call to awaiting_approval and blocks on a message-bus reply (gemini-cli/packages/core/src/scheduler/scheduler.ts:672-686). The best detail: an "always allow" answer is persisted not into a set but by narrowing and writing a new policy rule (updatePolicy, gemini-cli/packages/core/src/scheduler/scheduler.ts:691-700) — restricted to the current approval mode and anything more permissive (gemini-cli/packages/core/src/scheduler/policy.ts:133-145). The ordinary "Allow for this session" click (ProceedAlways, gemini-cli/packages/cli/src/ui/components/messages/ToolConfirmationMessage.tsx:295-297) never sets a persistScope — only the separate save-to-settings outcome, ProceedAlwaysAndSave, does that, and only when the folder isn't a trusted workspace with its own policies dir (gemini-cli/packages/core/src/scheduler/policy.ts:147-157, forwarded unchanged at gemini-cli/packages/core/src/scheduler/policy.ts:216-238). So createPolicyUpdater defaults an unset scope to Workspace, not User (gemini-cli/packages/core/src/policy/config.ts:728-731,770-773) — landing at priority fraction 950 (gemini-cli/packages/core/src/policy/types.ts:370-377) on tier 3 (gemini-cli/packages/core/src/policy/config.ts:83-86), i.e. effective 3.950 — still enough to outrank every default-tier rule next time. Only a saved, untrusted-folder "always allow" climbs to the User tier, 4.950.
Delegation is a real organ that flows through this same gate. AgentTool is published to the model as invoke_agent (gemini-cli/packages/core/src/tools/tool-names.ts:191) with params { agent_name, prompt } (gemini-cli/packages/core/src/agents/agent-tool.ts:43-73); built-in agents are registered in code (gemini-cli/packages/core/src/agents/registry.ts:284-287) and user agents load from Markdown-with-YAML-frontmatter files (gemini-cli/packages/core/src/agents/agentLoader.ts:322-356). LocalAgentExecutor (gemini-cli/packages/core/src/agents/local-executor.ts:120) runs the child in its own while (true) loop (gemini-cli/packages/core/src/agents/local-executor.ts:674-688) with a separate chat and a restricted tool list, and termination is protocol-based — the child must call complete_task or it is an ERROR_NO_COMPLETE_TASK_CALL (gemini-cli/packages/core/src/agents/local-executor.ts:362-371). Sub-agents are allowed by default (invoke_agent is a priority-50 allow in gemini-cli/packages/core/src/policy/policies/agents.toml:1-10), and the engine injects the subagent's name as a virtual tool alias so a policy can target one agent by name (gemini-cli/packages/core/src/policy/policy-engine.ts:562-570).
Context and memory
Every turn opens with a context check inside processTurn. Two systems coexist behind getContextManagementConfig().enabled (gemini-cli/packages/core/src/core/client.ts:643): a newer graph-based ContextManager pipeline and the classic tryCompressChat path. After either, the client computes the remaining budget as tokenLimit(model) - lastPromptTokenCount and refuses to send a request that would overflow, yielding ContextWindowWillOverflow (gemini-cli/packages/core/src/core/client.ts:696-715). Compression proper lives in ChatCompressionService.compress (gemini-cli/packages/core/src/context/chatCompressionService.ts:271-285): it no-ops until history exceeds 50% of the model's token limit (DEFAULT_COMPRESSION_TOKEN_THRESHOLD = 0.5 gemini-cli/packages/core/src/context/chatCompressionService.ts:41), then keeps the most recent ~30% (COMPRESSION_PRESERVE_THRESHOLD = 0.3 gemini-cli/packages/core/src/context/chatCompressionService.ts:47) and sends the older portion to a summarizer LLM that must produce a <state_snapshot> — with an explicit anchor instruction to merge any previous snapshot rather than lose it (gemini-cli/packages/core/src/context/chatCompressionService.ts:353-374).
Durable memory is GEMINI.md (DEFAULT_CONTEXT_FILENAME, gemini-cli/packages/core/src/tools/memoryTool.ts:11-16) — the direct analogue of CLAUDE.md. Discovery traverses upward from the cwd collecting GEMINI.md variants (gemini-cli/packages/core/src/utils/memoryDiscovery.ts:458-470), and the result is a HierarchicalMemory { global, extension, project, userProjectMemory } (gemini-cli/packages/core/src/config/memory.ts:7-13) flattened into labeled sections and passed into the system prompt via getCoreSystemPrompt(config, userMemory, ...) (gemini-cli/packages/core/src/core/prompts.ts:23-35).
对照 Claude Code
Same five organs, but the gate is rehoused from code into data. Each row maps Gemini CLI's mechanism to the Claude Code page that dissects the equivalent:
- The loop → /stories/loop/1/ and /stories/loop/2/. Same "call model → run tools → feed results back as a user message → loop" cycle (gemini-cli/packages/cli/src/nonInteractiveCli.ts:560), with the same
MAX_TURNS-style bound and loop detector — plus one extra organ: an LLMcheckNextSpeakerthat continues the loop even without tool calls (gemini-cli/packages/core/src/core/client.ts:880-904). - Tool anatomy → /stories/tools/1/. Same name + schema + validate + execute, but Gemini CLI reifies a validated call as a first-class frozen
ToolInvocation(gemini-cli/packages/core/src/tools/tools.ts:47-107) and runs dispatch through an explicit multi-status scheduler (gemini-cli/packages/core/src/scheduler/scheduler.ts:428-433) rather than executing inline. - The permission gate → /stories/tools/2/. Same three-way
allow / ask / denyverdict and same "ask → remember it" pattern, but where Claude Code's checks are code branches, Gemini CLI's are priority-sorted TOML data (gemini-cli/packages/core/src/policy/policy-engine.ts:209-212), even the approval modes are just rule filters (gemini-cli/packages/core/src/policy/types.ts:48-53), and "always allow" persists as a written rule (gemini-cli/packages/core/src/scheduler/scheduler.ts:691-700). - Compaction → /stories/context/1/. Threshold-triggered LLM summarization that preserves a recent tail, with a Gemini-specific twist: a mergeable
<state_snapshot>anchor so a second compaction folds the first in rather than dropping it (gemini-cli/packages/core/src/context/chatCompressionService.ts:353-374). - Memory → /stories/context/2/.
GEMINI.mdis the direct analogue ofCLAUDE.md, discovered hierarchically upward and injected into the system prompt (gemini-cli/packages/core/src/utils/memoryDiscovery.ts:458-470, gemini-cli/packages/core/src/core/prompts.ts:23-35). - Sub-agents → /stories/multiagent/1/. Same shape — a tool spawns a child loop with separate history and a restricted tool set, defined by Markdown frontmatter (gemini-cli/packages/core/src/agents/agentLoader.ts:322-356) — but with a hard completion protocol: the child must call
complete_taskor it errors (gemini-cli/packages/core/src/agents/local-executor.ts:362-371).
What to steal
-
Permissions as priority-sorted TOML data. The entire gate — YOLO, plan mode, per-mode transitions, MCP trust, "always allow" persistence — is declarative rules with numeric priorities in tiered bands (gemini-cli/packages/core/src/policy/policies/plan.toml:6-14, gemini-cli/packages/core/src/policy/policy-engine.ts:209-212). "Allow everything in YOLO" is a single data row, not a code path (gemini-cli/packages/core/src/policy/policies/yolo.toml:50-56), and a user's "always allow" click writes a narrowed rule back into the engine (gemini-cli/packages/core/src/scheduler/scheduler.ts:691-700). The gate becomes auditable, overridable, and diffable.
-
Model-controlled parallelism (
wait_for_previous). The harness injects await_for_previous: booleaninto every tool's JSON schema, so the model itself declares data dependencies between calls in a batch (gemini-cli/packages/core/src/tools/tools.ts:538-565); the scheduler batches contiguous parallelizable calls, forcing edit tools sequential regardless (gemini-cli/packages/core/src/scheduler/scheduler.ts:552-569). Parallelism becomes a schema-level contract instead of a harness-side guess. -
The inverse loop guard,
checkNextSpeaker. Beside the runaway detector, an LLM judge decides whether the model's last turn implies it should keep going, driving a synthetic"Please continue."continuation (gemini-cli/packages/core/src/core/client.ts:880-904). Detection and continuation are both first-class, model-assisted harness services rather than fixed stop conditions.
来源 · Source citations
- [1]
gemini-cli/package.json:3 - [2]
gemini-cli/packages/cli/package.json:11-13 - [3]
gemini-cli/packages/cli/src/gemini.tsx:350 - [4]
gemini-cli/packages/cli/src/nonInteractiveCli.ts:310-324 - [5]
gemini-cli/packages/cli/src/nonInteractiveCli.ts:560 - [6]
gemini-cli/packages/cli/src/ui/hooks/useGeminiStream.ts:2019-2055 - [7]
gemini-cli/packages/core/src/core/client.ts:79 - [8]
gemini-cli/packages/core/src/core/client.ts:614 - [9]
gemini-cli/packages/core/src/core/client.ts:747-763 - [10]
gemini-cli/packages/core/src/core/client.ts:880-904 - [11]
gemini-cli/packages/core/src/core/client.ts:910-917 - [12]
gemini-cli/packages/core/src/core/turn.ts:257-279 - [13]
gemini-cli/packages/core/src/tools/tools.ts:47-107 - [14]
gemini-cli/packages/core/src/tools/tools.ts:462-478 - [15]
gemini-cli/packages/core/src/tools/tools.ts:538-565 - [16]
gemini-cli/packages/core/src/tools/tools.ts:683-694 - [17]
gemini-cli/packages/core/src/tools/tool-registry.ts:271 - [18]
gemini-cli/packages/core/src/scheduler/scheduler.ts:195-223 - [19]
gemini-cli/packages/core/src/scheduler/scheduler.ts:428-433 - [20]
gemini-cli/packages/core/src/scheduler/scheduler.ts:552-569 - [21]
gemini-cli/packages/core/src/scheduler/scheduler.ts:609-615 - [22]
gemini-cli/packages/core/src/scheduler/scheduler.ts:639-648 - [23]
gemini-cli/packages/core/src/scheduler/scheduler.ts:650-666 - [24]
gemini-cli/packages/core/src/scheduler/scheduler.ts:672-686 - [25]
gemini-cli/packages/core/src/scheduler/scheduler.ts:691-700 - [26]
gemini-cli/packages/core/src/scheduler/policy.ts:53-72 - [27]
gemini-cli/packages/core/src/scheduler/policy.ts:133-145 - [28]
gemini-cli/packages/core/src/scheduler/policy.ts:147-157 - [29]
gemini-cli/packages/core/src/scheduler/policy.ts:216-238 - [30]
gemini-cli/packages/cli/src/ui/components/messages/ToolConfirmationMessage.tsx:295-297 - [31]
gemini-cli/packages/core/src/policy/config.ts:83-86 - [32]
gemini-cli/packages/core/src/policy/config.ts:728-731 - [33]
gemini-cli/packages/core/src/policy/config.ts:770-773 - [34]
gemini-cli/packages/core/src/policy/types.ts:10-14 - [35]
gemini-cli/packages/core/src/policy/types.ts:48-53 - [36]
gemini-cli/packages/core/src/policy/types.ts:60-65 - [37]
gemini-cli/packages/core/src/policy/types.ts:370-377 - [38]
gemini-cli/packages/core/src/policy/policy-engine.ts:85-196 - [39]
gemini-cli/packages/core/src/policy/policy-engine.ts:209-212 - [40]
gemini-cli/packages/core/src/policy/policy-engine.ts:253-256 - [41]
gemini-cli/packages/core/src/policy/policy-engine.ts:562-570 - [42]
gemini-cli/packages/core/src/policy/policy-engine.ts:577-632 - [43]
gemini-cli/packages/core/src/policy/policies/plan.toml:6-14 - [44]
gemini-cli/packages/core/src/policy/policies/plan.toml:76-81 - [45]
gemini-cli/packages/core/src/policy/policies/plan.toml:97-103 - [46]
gemini-cli/packages/core/src/policy/policies/plan.toml:131-136 - [47]
gemini-cli/packages/core/src/policy/policies/read-only.toml:30-56 - [48]
gemini-cli/packages/core/src/policy/policies/write.toml:36-40 - [49]
gemini-cli/packages/core/src/policy/policies/write.toml:30-34 - [50]
gemini-cli/packages/core/src/policy/policies/write.toml:47-57 - [51]
gemini-cli/packages/core/src/policy/policies/write.toml:65-69 - [52]
gemini-cli/packages/core/src/policy/policies/write.toml:88-100 - [53]
gemini-cli/packages/core/src/policy/policies/agents.toml:1-10 - [54]
gemini-cli/packages/core/src/policy/policies/yolo.toml:33-38 - [55]
gemini-cli/packages/core/src/policy/policies/yolo.toml:50-56 - [56]
gemini-cli/packages/core/src/core/client.ts:643 - [57]
gemini-cli/packages/core/src/core/client.ts:696-715 - [58]
gemini-cli/packages/core/src/context/chatCompressionService.ts:41 - [59]
gemini-cli/packages/core/src/context/chatCompressionService.ts:47 - [60]
gemini-cli/packages/core/src/context/chatCompressionService.ts:271-285 - [61]
gemini-cli/packages/core/src/context/chatCompressionService.ts:353-374 - [62]
gemini-cli/packages/core/src/tools/memoryTool.ts:11-16 - [63]
gemini-cli/packages/core/src/utils/memoryDiscovery.ts:458-470 - [64]
gemini-cli/packages/core/src/config/memory.ts:7-13 - [65]
gemini-cli/packages/core/src/core/prompts.ts:23-35 - [66]
gemini-cli/packages/core/src/tools/tool-names.ts:191 - [67]
gemini-cli/packages/core/src/agents/agent-tool.ts:43-73 - [68]
gemini-cli/packages/core/src/agents/registry.ts:284-287 - [69]
gemini-cli/packages/core/src/agents/agentLoader.ts:322-356 - [70]
gemini-cli/packages/core/src/agents/local-executor.ts:120 - [71]
gemini-cli/packages/core/src/agents/local-executor.ts:362-371 - [72]
gemini-cli/packages/core/src/agents/local-executor.ts:674-688