Read OSS

Gemini CLI を拡張する: Hooks、Skills、MCP、そして拡張システム

上級

前提知識

  • 本シリーズの第 1〜4 回
  • pub/sub パターンおよびストラテジーパターンの理解
  • MCP (Model Context Protocol) の基本的な概念

Gemini CLI を拡張する: Hooks、Skills、MCP、そして拡張システム

Gemini CLI はカスタマイズを前提に設計されています。エージェントのターンに割り込むシェルコマンドフックから、まったく新しいツール機能を追加する MCP サーバーまで、コードベースには複数の拡張ポイントが用意されています。この記事では、それらをすべて網羅します。5 つのコンポーネントからなる hook システム、4 段階の skill 優先度、OAuth を備えた MCP インテグレーション、HMAC 整合性検証を持つ extension パッケージングシステム、ストラテジーパターンによるモデルルーティング、そしてサブエージェントシステムです。

hook システムのアーキテクチャ

packages/core/src/hooks/ の hook システムは、ライフサイクルの重要なタイミングでユーザー定義のシェルコマンドを実行するために連携する、5 つのコンポーネントで構成されています。

flowchart TD
    EVENT[Lifecycle Event] --> EH[HookEventHandler<br/>dispatches events]
    EH --> HP[HookPlanner<br/>determines which hooks fire]
    HP --> HR[HookRunner<br/>executes as shell commands]
    HR --> HA[HookAggregator<br/>combines results]
    HA --> RESULT[Aggregated Result]
    
    REG[HookRegistry<br/>stores hook configs] --> HP

HookSystem クラスがこれらを組み合わせています。

constructor(config: Config) {
    this.hookRegistry = new HookRegistry(config);
    this.hookRunner = new HookRunner(config);
    this.hookAggregator = new HookAggregator();
    this.hookPlanner = new HookPlanner(this.hookRegistry);
    this.hookEventHandler = new HookEventHandler(
        config, this.hookPlanner, this.hookRunner, this.hookAggregator,
    );
}

HookRegistry は複数のソース(ユーザー設定、ワークスペース設定、extension)から hook の設定を収集して保持します。各 hook には、監視するイベント、任意のマッチャーパターン、順次実行するかどうかのフラグを指定します。

HookPlanner は、特定のイベントに対してどの hook を発火させるかを決定します。レジストリを参照し、マッチャー式を評価します。

HookRunner は hook をシェルコマンドとして実行します。hook は JavaScript 関数ではなく外部プロセスです。これにより、特定の言語に縛られない拡張性とセキュリティの境界が確保されます。hook スクリプトは環境変数と stdin を通じてコンテキストを受け取ります。

HookAggregator は、同じイベントに対して複数の hook が発火した際の結果を統合します。結果にはシステムメッセージ、注入する追加コンテキスト、制御の決定(stop、block、continue)を含めることができます。

HookEventHandler は全体を取りまとめるディスパッチャーで、fireSessionStartEventfireBeforeAgentEventfireAfterAgentEvent などの型付きメソッドを提供します。

8 つのライフサイクルイベントは次の通りです。

イベント 発火タイミング
SessionStart セッションの開始または再開時
SessionEnd セッションの終了時
BeforeAgent プロンプトに対して最初のモデル呼び出しが行われる前
AfterAgent ツールの呼び出しが残っていない状態でモデルが応答した後
BeforeModel 個々の API 呼び出しの前
AfterModel 個々の API レスポンスの後
BeforeToolSelection ツール一覧をモデルに渡す前
PreCompress チャット履歴が圧縮される前

第 2 回で解説したとおり、BeforeAgentAfterAgent は実行の停止、理由付きのブロック、コンテキストの注入が行えます。BeforeModel はリクエスト設定を変更したり、合成レスポンスを返したりすることができます。BeforeToolSelection はモデルが参照するツールの一覧を変更できます。

ヒント: hook はシェルコマンドとして実行されるため、どの言語でも記述できます。Python スクリプト、Go バイナリ、bash のワンライナーでも問題ありません。hook は stdin で JSON コンテキストを受け取り、stdout に JSON 形式の結果を返します。

Skills: 探索と優先度

skill は、セッション中にユーザーが有効化できる、特化したプロンプト・ツール設定です。SkillManager は、以下の 4 つの場所から優先度の高い順に skill を探索してロードします。

flowchart BT
    B[1. Built-in skills<br/>Lowest precedence] --> E[2. Extension skills]
    E --> U[3. User skills<br/>~/.gemini/skills/]
    U --> W[4. Workspace skills<br/>.gemini/skills/<br/>Highest precedence]

47 行目discoverSkills() メソッドは次の順序で処理します。

  1. Built-in skillsskills/builtin/ ディレクトリに格納(isBuiltin = true が付与される)
  2. Extension skills — 有効な extension から取得(extension.skills
  3. User skills~/.gemini/skills/ および ~/.gemini/.agents/skills/ から取得
  4. Workspace skills.gemini/skills/ および .gemini/.agents/skills/ から取得(フォルダが信頼済みの場合のみ)

同名の skill が存在する場合、addSkillsWithPrecedence() によって優先度の高い場所のものが低い場所のものを上書きします。これにより、workspace skill が built-in skill を上書きでき、プロジェクト固有のカスタマイズが可能になります。

セキュリティ上の重要な考慮点として、workspace skill はフォルダが信頼済みの場合にのみロードされます。信頼されていないプロジェクトからエージェントの動作に影響を与える skill が注入されることはありません。

MCP サーバーインテグレーション

MCP (Model Context Protocol) インテグレーションにより、Gemini CLI は stdio または HTTP を通じて外部のツールサーバーに接続できます。インテグレーションは OAuth 認証、サーバー探索、動的なツール登録にまたがって機能します。

MCPOAuthProvider は、認証が必要な MCP サーバーに対する OAuth/PKCE フローを処理します。認可サーバーのメタデータ探索、トークンの取得・更新・保存を管理します。

sequenceDiagram
    participant Config as Config.initialize()
    participant MCM as McpClientManager
    participant OAuth as MCPOAuthProvider
    participant MCP as MCP Server
    participant TR as ToolRegistry
    participant PE as PolicyEngine
    
    Config->>MCM: Connect to configured servers
    MCM->>OAuth: Authenticate (if needed)
    OAuth->>MCP: PKCE auth flow
    MCP-->>OAuth: Access token
    MCM->>MCP: listTools()
    MCP-->>MCM: Tool schemas
    MCM->>TR: registerTool(DiscoveredMCPTool)
    Note over TR: mcp_serverName_toolName
    
    Note over PE: Policy rules with<br/>mcp_serverName_* wildcards<br/>control access

MCP ツールは DiscoveredMCPTool でラップされます。これは BaseDeclarativeTool を継承し、ポリシーマッチングに使用する MCP 固有のメタデータ(サーバー名、ツールアノテーション)を追加したものです。MCP_TOOL_PREFIX で定義された mcp_ プレフィックスの規約により、built-in ツールとの名前衝突を防ぎつつ、mcp_myserver_* のようなワイルドカードポリシーを記述できます。

第 4 回で解説したポリシーエンジンのワイルドカードマッチングは、MCP の 3 つのパターンに対応しています。

  • mcp_* — 任意のサーバーのあらゆる MCP ツールにマッチ
  • mcp_serverName_* — 特定のサーバーのすべてのツールにマッチ
  • mcp_serverName_toolName — 特定のツールにマッチ

extension システムと整合性検証

extension は複数の拡張機能をインストール可能なパッケージにまとめたものです。1 つの extension に hooks、skills、MCP サーバー設定、スラッシュコマンド、テーマ、ポリシールールを含めることができます。

セキュリティ上の重要な機能として、HMAC 整合性検証システムがあります。IntegrityKeyManager は extension のメタデータに署名するための 256 ビットの秘密鍵を管理します。

class IntegrityKeyManager {
    private readonly fallbackKeyPath: string;
    private readonly keychainService: KeychainService;
    private cachedSecretKey: string | null = null;
    
    async getSecretKey(): Promise<string> {
        if (this.cachedSecretKey) return this.cachedSecretKey;
        
        if (await this.keychainService.isAvailable()) {
            try {
                this.cachedSecretKey = await this.getSecretKeyFromKeychain();
                return this.cachedSecretKey;
            } catch (e) {
                // Fall back to file-based storage
            }
        }
        
        this.cachedSecretKey = await this.getSecretKeyFromFile();
        return this.cachedSecretKey;
    }
}

鍵は OS のキーチェーン(KeychainService 経由)に優先的に保存され、利用できない場合は 0o600 パーミッションのファイルにフォールバックします。extension がインストールされるとそのメタデータがこの鍵で署名され、ロード時に署名を検証することで改ざんを検出します。

graph TD
    subgraph "Extension Package"
        H[Hooks]
        S[Skills]
        MCP[MCP Servers]
        CMD[Commands]
        TH[Themes]
        POL[Policies]
    end
    
    INST[Extension Install] --> SIGN[HMAC Sign with secret key]
    SIGN --> STORE[Store signed metadata]
    
    LOAD[Extension Load] --> VERIFY[Verify HMAC signature]
    VERIFY --> ACTIVE[Activate extension]
    VERIFY --> REJECT[Reject tampered extension]
    
    subgraph "Key Storage"
        KC[OS Keychain<br/>preferred]
        FILE[File ~/.gemini/<br/>fallback, 0600]
    end

ヒント: システム移行後に extension の検証が原因不明で失敗する場合は、キーチェーンが正しく引き継がれているか確認してください。~/.gemini/ にあるフォールバック鍵ファイルが、元のシステムで extension に署名した鍵と異なっている可能性があります。

拡張ポイントとしてのモデルルーティング

各ターンをどのモデルが処理するかを決定するモデルルーティングは、Composite Strategy パターンに従って実装されています。ModelRouterService は 7 つのストラテジーを優先度順にチェーンします。

flowchart LR
    REQ[Routing Request] --> F[FallbackStrategy]
    F --> O[OverrideStrategy]
    O --> A[ApprovalModeStrategy]
    A --> GC[GemmaClassifierStrategy]
    GC --> C[ClassifierStrategy]
    C --> N[NumericalClassifierStrategy]
    N --> D[DefaultStrategy<br/>terminal]

各ストラテジーは RoutingContext(会話履歴、現在のリクエスト、要求されたモデル)を受け取り、RoutingDecision を返すか、次のストラテジーに処理を委ねます。

  • FallbackStrategy — 主モデルが利用できない場合に有効化される
  • OverrideStrategy — 明示的なモデル切り替え(例: /model flash)を処理する
  • ApprovalModeStrategy — PLAN モードに適したモデルを選択する
  • GemmaClassifierStrategy — ローカルの Gemma モデルをルーティングの分類に使用する
  • ClassifierStrategy — LLM を使った汎用的な分類を行う
  • NumericalClassifierStrategy — 数値スコアリングのヒューリスティクスを使用する
  • DefaultStrategy — 末端ストラテジー。常に設定済みモデルを返す

CompositeStrategy はすべてのストラテジーをラップし、末端ストラテジーが必ず結果を返すことを保証します。これは古典的な Chain of Responsibility パターンで、各ストラテジーはリクエストを処理するか、次へ渡すかのどちらかを行います。

サブエージェントと派生 MessageBus

サブエージェントシステムにより、Gemini CLI はブラウザ自動化などの特化したタスクに対して子エージェントを生成できます。サブエージェントは親エージェントとの干渉を避けるため、独自のツールレジストリとメッセージバスを必要とします。

第 1 回で最初に確認したように、46〜72 行目MessageBus.derive() はスコープを絞り込んだ子バスを生成します。

derive(subagentName: string): MessageBus {
    const bus = new MessageBus(this.policyEngine, this.debug);
    bus.publish = async (message: Message) => {
        if (message.type === MessageBusType.TOOL_CONFIRMATION_REQUEST) {
            return this.publish({
                ...message,
                subagent: message.subagent
                    ? `${subagentName}/${message.subagent}`
                    : subagentName,
            });
        }
        return this.publish(message);
    };
    // Delegate subscriptions to parent
    bus.subscribe = this.subscribe.bind(this);
    // ...
}

派生バスは publish をオーバーライドし、確認リクエストにサブエージェント名のプレフィックスを付与します。その他の操作(subscribe、unsubscribe、on、off)はすべて親バスに委譲されます。これにより、サブエージェントのツール確認は親の UI 上で適切な帰属情報(例: browser/navigate)とともに表示される一方、通常のイベントハンドリングは通常通り流れます。

サブエージェントシステムは AgentLoopContext とも連携します。各サブエージェントは独自のツールレジストリとメッセージバスを持つ派生コンテキストを受け取りながら、Config とサンドボックスマネージャーは親と共有します。ここで、第 1 回で紹介した AgentLoopContext インターフェースの設計が活きています。このインターフェースを受け取るコンポーネントは、親エージェントとサブエージェントのいずれのコンテキストでも同一に動作します。

次回の最終回では、これらすべてのシステムの上に構築された 2 つの主要なインタラクションサーフェス、React/Ink ベースのターミナル UI とプログラマティックな SDK について解説します。