実験的 Mods
Qwen Code はヘッドレスセッションで Claude Code Mod API の小規模なサブセットを実行できる。 この機能はオプトインであり、信頼されたワークスペースと、有効化された Claude または ネイティブ Qwen 拡張機能が必要である。Agent Plugins v1 パッケージは引き続きフックを無視する。
以下の設定を Qwen の設定に追加する:
{
"experimental": { "mods": true }
}以下のファイルを含むプラグインディレクトリを作成する:
hello-mod/
.claude-plugin/plugin.json
hooks/hooks.json
hooks/register.js.claude-plugin/plugin.json:
{ "name": "hello-mod", "version": "1.0.0" }hooks/hooks.json(モジュールパスはこのファイルからの相対パス):
{ "modules": ["./register.js"] }hooks/register.js:
let calls = 0;
export function register(on) {
on('session.start', async ($, e, next) => {
await $.command.register({
name: 'hello',
description: 'Say hello without a model request',
});
return next(e);
});
on('command.run', { command: 'hello' }, ($, e) => ({
text: `Hello ${e.args || 'world'}! Call ${++calls}.`,
}));
on('session.end', ($, e, next) => {
$.ui.log(`Finished ${e.sessionId}`, { to: 'debug' });
return next(e);
});
}プラグインをインストールし、そのコマンドを実行する:
qwen extensions install ./hello-mod --consent
qwen -p '/hello Qwen' --output-format json起動時には通常の CLI 認証設定が引き続き必要である。Mod コマンド自体はモデルリクエストを行わない。Text、JSON、stream-json のヘッドレス出力 がサポートされている。stream-json プロセスはターン間でモジュール変数を保持する。
サポートされるサブセット
| 表面 | 動作 |
|---|---|
| パッケージング | 1 つの .js、.mjs、.cjs、.ts、.mts または .cts ESM エントリ。インストールされたプラグイン内の相対静的インポートと再エクスポート |
| エントリ | register(on, options)。非同期登録は待機される。型付きの非シークレットオプションはコピーされ、深くフリーズされる |
| ライフサイクル | すべてのモジュールは初期の session.start チェーン前に登録される。終了時と会話置換時に session.end が呼ばれる。クリア/再開ではモジュールはリロードされない |
| コマンド | session.start 中に name、description、任意の argumentHint を使用して登録する。プラグイン間で登録された Mod コマンドをインターセプトする。{text} または {} を返す |
| ツール | tool.call で既存のメインセッションツールをインターセプトする。tool.check でネイティブのパーミッション判定を調査・制限する |
| ミドルウェア | パターンと再帰的マッチャー。明示的で繰り返し可能な next(e)。.catch とプラグインごとの失敗回復。フリーズされた入力とホスト識別 |
| API イベント | command.register と ui.log 呼び出しはホスト提供のオリジンとともにミドルウェアチェーンを通過する |
| ロギング | $.ui.log は void を返し、配信をキューに入れる。トランスクリプトログはテキストモードでは stderr を、JSON モードでは system/ui_log を使用する。デバッグログはデバッグロガーに留まる。いずれもモデル入力にはならない |
| 分離 | セッションワーカー内のプラグインごとに独立した QuickJS 環境。Node、ファイルシステム、ネットワーク API はなし。メモリ、スタック、ソース/出力、実行の制限 |
これはサポートされるサブセットであり、既存の Claude Mods との完全な互換性ではない。
JSX/TSX、ランタイムパッケージインポート、組み込みコマンドのインターセプション、ツール登録、アクティブな $.tool API、コンテキスト、
state/store、レンダリング、ストリーミングイベント、ホットリロードは実装されていない。
コマンドは context、exitCode、エンジン参照をまだ返せない。
2 つの Mod が同じコマンド名を登録することはできない。後の登録は拒否される。
Qwen の組み込みコマンドとの競合は Qwen のコマンド競合ポリシーに従う。
拡張コマンドは extensionName.commandName にリネームされる。無効化されたスラッシュ
コマンド名は結果の名前に対してマッチングされる。
ミドルウェアと回復
パターンは session.start、session.end、command.run、
command.register、ui.log、tool.call、tool.check から選択する。完全な名前、command.* のようなプレフィックス、
*、または !session.* のような単一の否定を使用する。他の具体的なイベントと !*、!!... のパターンは拒否される。
マッチャーは部分オブジェクトである。指定されたすべてのフィールドが一致しなければならない。配列は
代替値であり、ネストされたオブジェクトは自身のフィールドにマッチし、正規表現は
フィールドの文字列表現をテストする。マッチャーは登録時にコピーされる。
最大 8 レベル、256 ノードまで許可される。状態を持つ g/y 正規表現とネストされた
量指定子は拒否される。65,536 文字より長い正規表現入力は保守的にマッチし、
判断をフックに委ねる。ネストされたオブジェクトはデータであり、startsWith のような
演算子ではない。
1 つの正確なイベントは、マッチャーなしの 1 つの登録と、マッチャー付きの複数の登録を持てる。
等しいマッチャーも含む。各ワイルドカードまたは否定パターンは 1 回だけ登録できる。
プラグイン内では、登録順がチェーンを決定する。コンシューマーは宣言された依存プロバイダーより前に実行される。
ハンドラーは早期に回答でき、session.start でも可能であり、後続のハンドラーの実行を防ぐ。
command.run ではコマンドの args だけが書き換え可能である。ライフサイクルと
コマンドの識別フィールドは固定されたままとなる。
ツールのインターセプション
tool.call は Qwen ツールの引数を tool と tool_use_id と並んでフラットなフィールドとして受け取る。
名前、引数スキーマ、出力レコードは Qwen のネイティブ
コントラクトを使用するため、Claude の組み込みツール向けに書かれた Mod は適応が必要になる場合がある。
独立した tool.result イベントは存在しない。await next(e) が返す値を調査または変更する:
export function register(on) {
on('tool.call', { tool: 'write_file' }, async ($, e, next) => {
if (!e.file_path.endsWith('.txt'))
return { deny: 'Only text files are allowed' };
return next({ ...e, content: e.content.trim() });
});
on('tool.check', async ($, e, next) => {
const verdict = await next(e);
if (e.tool === 'run_shell_command') {
return { decision: 'deny', reason: 'Shell is disabled by this Mod' };
}
return verdict;
});
}通常の tool.call の next は毎回新しいネイティブツール試行を開始する。バリデーション、
クラシックフック、パーミッション、承認、呼び出しガードがすべて引き続き適用される。
追加の試行は --max-tool-calls に対してカウントされる。catch replay は試行を追加しない。
ツールミドルウェアを持つセッションはツール呼び出しを直列化する。並列
next 呼び出しも含む。書き換えられた引数が副作用を導入する可能性があるためである。1 つのルート
呼び出しは最大 64 のネイティブ試行と 32 の未解決ホスト操作を許可する。
60 秒のルート制限はネイティブ実行と承認待機の時間を含む。未解決の next が完了する前に
戻ると、その作業はキャンセルされ、解決を待つ。
tool.check はネイティブバリデーションと
クラシック入力書き換えの後に {tool, input, tool_use_id} を受け取る。すべてのフィールドは固定されている。その next は宣言的な
{decision: 'allow' | 'ask' | 'deny', reason?} をツールを実行せずに返す。
Qwen は回答を制限的にマージする。Mod はネイティブの deny または
ask を allow にアップグレードできない。Mod の ask は現在の承認モードの既存の決定
フローに従う。必要な人間の対話とクラシックの強制 ask は引き続き権限を持つ。
これは Claude のコンシューパーパーミッションオーバーライドよりも意図的に厳しくなっている。
ネイティブの PermissionRequest フックが引数を再構築した場合、それらは再度
チェックされる。Mod が作成した rule、hook、ceiling はホストパーミッションを作成できない。
{deny: reason} を返してブロックする。{result: value} を返して JSON 結果を
実行せずに提供する。ネイティブの next は {ref, result, text, isError?} を返す。
result はネイティブ関数の応答レコードであり、一般的には {output: ...} または
{error: ...} である。変更されていない result を持つ返された ref は元の
出力を保持する。画像と出力メタデータも含む。その参照の隣の text または error フラグを変更しても
それは変わらない。ホストの実行事実は
以前の結果が選択された場合でも、すべての試行で保持される。出力を変更するには、
新しい result を返す:
{result: {output: 'Replacement text'}}。文字列の
output/error フィールドのみを含むレコードはその形状を保持する。他の JSON は output 文字列に完全にシリアライズされる。
ネイティブの出力バジェットが引き続き適用される。ホストが関数応答を所有する。
JSON が ID、メディアパート、実行事実に解釈されることはない。
不明な ref と isReadOnly 主張は来歴を付与しない。Mod ペイロードに対して大きすぎる結果には
省略通知と ref が含まれる。そのペアを
変更せずに返すことで、完全なネイティブ応答が保持される。
ネイティブエラー、承認要件、キャンセルは Mod result を変更しても 正常な実行に変換できない。next 後の拒否は ツールの効果を元に戻せない。Qwen はツールがすでに実行されたと報告する。同じことが 以前の試行が実行され、後の試行が拒否された場合にも適用される。コンテキスト注入 とリクエストメタデータはサポートされていない。
ツールの識別、呼び出し ID、同意、ホストメタデータは固定されている。tool、tool_use_id、agentId、consent、
requestMeta、$shadowed と衝突するネイティブ引数名は
ホストが作成した固定の $shadowed
レコードに保持され、実行時に復元される。有効な遅延 tool_call リクエストは
解決されたターゲットの名前と引数を公開し、
ラッパーのネイティブパーミッションゲートを保持する。無効なラッパー、不明なツール、固定ポリシーの呼び出し、
合成 structured_output ターミナルツールは元のパスをたどる。
tool_search と search_memory もネイティブのままである。ゲストが作成した結果が
ネイティブの出力バジェット免除を継承しないようにするためである。
Goal ツール呼び出しもネイティブパスを保持する。このステージでは Mod が作成した
結果を Goal のカタログに外部証拠として入れることはできない。
子エージェントとネストされたコードモードスケジューラーはこのミドルウェアを継承しない。
コマンド回復
export function register(on) {
on('command.run', { command: /^hello/ }, async ($, e, next) => {
const result = await next({ ...e, args: e.args.trim() });
return { text: `[wrapped] ${result.text || ''}` };
}).catch(($, e, next) => next(e));
}通常の next はすべての呼び出しで下流を実行する。プラグインの登録は
1 つのレイヤーを形成し、失敗レコードと時間バジェットを共有する。スロー、タイムアウト、
無効な結果の場合、その catch が登録順に試行される。いずれかが
未定義でない回答を返すか next を呼ぶまで。未定義を返すか、next を呼ばずに
スローした catch は、次にマッチする catch が実行されるのを許可する。Claude Code
2.1.296 と同様に、無効な回答または next 後の失敗は catch 選択を終了し、
共有フォールバックレコードを使用する。後続の catch は試行されない。catch 内の next.called は
プラグインが回復開始前に次のプラグインに cross into したことを意味し、
自身の別の登録への移動ではない。
プラグインがすでに下流を呼び出していた場合、回復は最後に
開始された呼び出しを待機してリプレイする。その拒否も含む。リプレイ引数は無視される。
呼び出していなかった場合、catch の最初の next が下流を 1 回実行し、後続の呼び出しはそれをリプレイする。
失敗した catch は同じレコードを使用する。成功した catch がない場合、プラグインは
スキップされるか、その記録された結果が保持される。誰も応答しない登録されたコマンドは
明示的に失敗する。モデルには送信されない。
next は event、is(pattern)、フリーズされた origin、signal、および ms と remainingMs を持つ
新しいフリーズされた budget スナップショットを公開する。catch は追加で called
と error.kind(throw、timeout、re-entry)を受け取る。エンジンイベントはオリジン
{ plugin: 'engine', tier: 'core' } を使用する。Mod API イベントは呼び出し元のプラグインを
tier user で識別する。マネージドティアの next.to、トレーシング、カスタム名前空間の lent
回復は利用できない。
API イベントはチェーンの先頭から開始する。その入力はコマンド仕様または
{text, to} ログペイロードである。next(e)、{value} を返して応答する、または {deny: reason} を
返して API 呼び出しを拒否する。即座のセルフリエントリはその登録をスキップする。アクティブな
祖先登録は catch を使用できる。リエントリ catch は next を呼べない。その next は
元のイベントを 1 回再利用する。ui.log は void 通知のままである。
await $.ui.log(...) はインターセプターを待機しない。ヘッドレス境界は
出力を返す前にキューに入れられた通知を drain する。
通常のプラグインレイヤーはクロスプラグインの next または API 呼び出しの待機時間を除いて
10 秒の自身の時間を持つ。待機中の void ui.log 通知は
呼び出し元のバジェットを一時停止しない。catch とリエントリ回復は 1 秒のバジェットを使用する。
リエントリディスパッチはこの短いバジェットを
そのプラグイン内の他の登録と共有する。Qwen はさらに各ルートディスパッチを 60 秒に、
各中断されないゲスト実行スライスを 10 秒にキャップする。すべての session.end 作業は
1.5 秒を共有する。早期に完了する外部ハンドラーは保留中の下流
作業を放棄する。キャンセルはフォールバック操作を開始しない。保持された next/API 関数は
その呼び出し終了後に作業を実行できない。250 ミリ秒以内に
キャンセルを確認できないワーカーは終了される。新しいセッションが必要になる。
Mod 環境は最大 32、登録コマンドは最大 64、 プラグインごとの登録は最大 128、ルート呼び出しごとのルート/API イベントチェーンは最大 4,096。別の プラグインを通過しても追加のイベントは消費されない。各環境は 64 MiB のメモリ制限を持つ。ログとメッセージペイロードも制限される。登録、コンパイル および設定の失敗は引き続き初期化を拒否する。通常のフックの失敗は モジュール状態をリセットせずに回復できる。ゲストの promise ジョブは ホストリクエストがアクティブな間のみ実行される。キューに入れられた通知の drain を含む。
このバージョンでは、Mod は TUI、ACP、マネージド/SSH、サンドボックス、またはセッションエージェントのエントリポイントでは
実行されない。セーフモード、ベアモード、disableAllHooks も読み込みを防ぐ。
プラグインコード、オプション、拡張機能のアクティベーションを変更した後は再起動する。すでに読み込まれた
コマンドは実行前に参加するすべてのプラグインを再チェックし、変更された
または無効化されたミドルウェアチェーンを拒否する。外部設定の編集は再起動後に有効になる。
絶対パスまたは外部のフックマニフェストパスを持つネイティブ Qwen 拡張機能は
引き続きクラシックフックローダーのみを使用する。変換がすでに
絶対パスまたは拡張機能の外側のために無視している Claude フックパスも
Mod 検出によってスキップされる。Mod マニフェストは拡張機能内の相対パスでなければならない。
TypeScript と相対インポート
TypeScript を使用するにはフックエントリを ./register.ts に変更する。インポートと
再エクスポートは相対パス、拡張子なしパス、ディレクトリの index ファイル、
または TypeScript ファイルの JavaScript 表記を使用できる(./helper.js は
helper.ts に解決される場合がある)。正確なファイルが優先される。次に .js → .ts、.mjs → .mts、
.cjs → .cts の置換が続く。その後 .ts、.js、.mjs、.cjs、.mts、
.cts の候補が続き、各ファイルを試してからそのディレクトリインポートを試す。6 つすべての
サフィックスは ESM を使用する。.cjs と .cts も含む。モジュールサイクルと 1 つのプラグイン内の共有インポート
状態がサポートされている。
コンパイルはキャンセル可能なワーカー内で固定オプションを使用する。型チェックは行わず、
tsconfig.json、パッケージリゾルバー、ビルドプラグインは読み込まない。型限定
インポート(claude-code からの型を含む)は消去される。claude-code からのランタイム値インポートは
このサブセット外の API を必要とし、拒否される。パッケージ/Node/URL インポート、動的インポート、require、JSX、インポートされたトップレベル
await も拒否される。ゲストコードは引き続き QuickJS でのみ実行される。
プラグインオプション
プラグインマニフェストで userConfig を宣言する:
{
"name": "hello-mod",
"version": "1.0.0",
"userConfig": {
"greeting": {
"type": "string",
"title": "Greeting",
"description": "Greeting used by /hello",
"default": "Hello"
}
}
}ユーザーまたはシステム設定でオプションを設定し、register の
2 番目の引数から options.greeting を読み取る:
{
"experimental": { "mods": true },
"pluginConfigs": {
"hello-mod": { "options": { "greeting": "Welcome" } }
}
}フィールドには type、title、description が必要である。サポートされる型は string、
number、boolean、directory、file である。オプションの制約には
required、default、数値の min/max、文字列の multiple、文字列の
options 選択肢がある。ファイルとディレクトリの値は文字列であり、存在
チェックや追加のファイルシステムアクセスはない。sensitive: true はデフォルトが提供されている場合でも
明示的にサポートされない。シークレットストレージや設定 UI は存在しない。
Qwen は SystemDefaults → User → System の優先順位を使用し、個別のオプション
キーをマージする。ワークスペース設定は信頼されたワークスペースでも無視される。Qwen は
マーケットプレイスの来歴を持たない拡張機能をインライン ID にマッピングする。それらは
name と name @inline を受け入れる。後者が各設定ソース内で優先される。
マーケットプレイスプラグインは実際の name @marketplace ID を使用する。より高い
優先順位のソースが、より低い優先順位のソースからのいずれのローカルエイリアスよりも優先される。
調査された Claude の実装と同様に、不足しているオプション値はデフォルトを持たない限り '' になる。
オプションの '' も型チェックをバイパスする。必須
値は不足または '' を拒否する。false と 0 は有効なままとなる。無効な保存済みの
文字列選択肢はデフォルトにフォールバックする。空でないスキーマでは、不明なオプション
キーも通過する。文字列、数値、ブール値、文字列配列に制限される。
空または空のスキーマは {} を受け取る。オプションは再起動までフリーズされたままとなる。
インストールされた依存関係
マニフェストの dependencies はプラグイン名、name @marketplace、または
{ "name": "base", "marketplace": "team", "version": "^1.0.0" } のようなオブジェクトを受け入れる。
ベア名はコンシューマーのマーケットプレイスを参照する。同じ名前のインストールされたインラインプラグインが
マーケットプレイスの依存関係をオーバーライドする場合がある。バージョンチェックなしで。
それ以外の場合、バージョン範囲はインストールされた拡張機能の
マニフェストバージョンに対してチェックされる。Mods なしの通常の拡張機能が依存関係を満たせる。
それらの推移的依存関係は選択された Mod から到達可能なときにチェックされる。
無関係な通常の拡張機能の依存関係宣言は参加しない。
依存関係はすでにインストール、有効化、この呼び出しに対して選択されている必要がある。 Qwen は自動的にフェッチ、インストール、有効化を行わない。選択された Mod は Mod 間の直接依存関係に対して安定したコンシューマー優先の順序で登録される。 通常のプロバイダーは登録ノードではない。各 Mod は 個別のレルムを持つ。イベントはこの順序でそれらのハンドラーを通過し、 モジュール変数を共有せずにクロスプラグインのインターセプションを可能にする。
不足、曖昧、非互換、または循環依存関係は Mod ワーカーが開始する前にスタートアップを拒否する。無効な Mod オプションもスタートアップを拒否する。これは Claude の選択的依存関係降格とサイクル順序フォールバックとは異なる。Qwen は実験的サブセットのセッション全体スタートアップ失敗ポリシーを保持する。
リソース制限
リーダーはインストールされた拡張機能内の 256 KiB までの通常ファイルを受け入れ、 そのルート内のシンボリックリンクを拒否する。グラフは 64 ファイル、1 MiB の入力、1 MiB のコンパイルコードに制限される。これらの境界とシンボリックリンク拒否は Claude よりも厳しい。各ゲストは 64 MiB のメモリ制限と 1 MiB のスタック制限を持つ。 上記の実行バジェットには、すべての ワーカー初期化、コンパイル、スタートアップフックを含む 60 秒のスタートアップ制限が含まれる。ゲストメモリ 制限はコンパイラのホストメモリをカバーしない。
author API は Claude Code 2.1.296 の宣言と 公式 Mod ドキュメント に基づいている。