Claude Codeに新しくできたDynamic Workflowっていう機能がありまして。簡単に言うと数十個のAIエージェントにブレストさせる機能なんですが、それにGrokを組み込もうとして失敗した記録なんですが、セキュリティ的な意味で貴重な発見がありましたのでシェアします。
先に結論だけ
Claude CodeのAuto-Mode分類器は、実行しようとしているコードの「中身」、それもコメントに書いた“意図”まで読んでます。 「分類器のデータ持ち出しブロックを回避する目的のラッパー」とコメントに書かれたスクリプトは、実行も編集も拒否されマルウェア扱いされました。
やりたかったこと
Grok Buildが案外よかった
X(旧twitter)率いるxAIのGrokが、コーディングエージェントCLIとしてGrokBuildというCLIツールをベータ版で提供しています。いまのところ使用できる推論モデルはgrok-buildというコーディング専用の高速化モデルだけなんですけど、Grok4.3はZedからAPIで使った感じだとOpus4.7と同等程度に推論力がありました。(弱点もありましたけどそれはまた別な記事で)
Opus4.8には劣るけどSonnet4.6より優秀な補佐役として、Grok4.3は有望だと思うんです。コスト的にも。
Claude CodeのDynamic Workflowもよかった
先日のClaude Codeのアップデートで、Dynamic Workflowという、サブエージェントを大量に並べてスクリプトで決定的にオーケストレーションする仕組みが実装されました。一時話題になったけどうまくいかなかったSwarmの完全版みたいな印象。
要件書を与えて「workflowでこの要件を実装して」のワンプロンプトで、現在の実装や依存ライブラリを調べ、新規で必要な実装や既存実装で再利用可能なモジュールなどを多角的に分析して、10ターン分くらいの実装を自走的にリーダブルコードで仕上げてくれる良機能です。
ただし死ぬほどトークンを食う。
そう。
死ぬほどトークンを食うんです。
具体的に言うと、Androidアプリの機能追加5個でMax5の5時間リミットを30分で食い尽くすくらい、どか食い大好きクロードさんです。
トークンがないなら別なAIを使えばいいじゃない(フラグ)
というわけで、優秀な機能なんですけど節約のため、ここにGrokBuild(xAI)を「独立した第二意見を出すオブザーバー」として混ぜたかったんです。節約の他にも同じAnthropic製モデル同士でレビューし合っても身内感が拭えないので、他社のモデルにセカンドオピニオンを出させたい、っていう思惑もありました。
で、試したこと
オブザーバとしてgrokも使ってworkflowでこの機能を検討してと指示- WorkflowのスクリプトはサンドボックスJSで、
agent()しか呼べない。grok -pなどのCLIを直接叩けない。
- WorkflowのスクリプトはサンドボックスJSで、
- Bashを持つClaude子エージェントを「ブリッジ」にして、
grok -pを読み取り専用で起動させる- 手動(メインセッション)だと普通に動いた。なのに Workflow のブリッジ経由だと、Claude Code の分類器に「Data Exfiltration(プライベートrepoのソースを外部AIへ送信)」判定で拒否された。
- Grokを起動するスクリプトを作成しSkillsとして登録、子エージェントに使わせる
- 同上。
要は、エージェントが私有ソースを外部 AI に送る動きが、Workflowのサブエージェント文脈だと弾かれる構造でした。
SkillsがダメならMCPを使えばいいじゃない(フラグ)
で、今回の思いつきです。
生の
grok -pをシェルで叩くからアドホックな「持ち出しコマンド」に見えるんだ。これを MCP サーバでラップして、ユーザが.mcp.jsonに登録してsettings.jsonでmcp__grok__*を明示 allow すれば、「正式に許可された名前付きツール」になる。グレー判定じゃなく明示 allow ルールで通せるはず。
実際settings.jsonに記載されているmcp__plugin_sqlew_sqlew__*など、別のMCPのallowlist自体はちゃんとworkflow内部からも機能しているようでしたので、これでいけるんじゃないかと。
ということで、ClaudeCodeに指示して、まずは疎通テストのスクリプトを書かせてみたんです。
コード全文
#!/usr/bin/env node
// Grok Observer MCP server (stdio, zero-deps).
//
// 目的: `grok -p` を MCP ツール `grok_observe` としてラップする。
// Workflow のサブエージェントは権限分類器が厳しく、Bash 経由の grok 呼び出しを
// 「Data Exfiltration(プライベートrepoのソースを外部AI=Grok/xAIへ送信)」として拒否する。
// これを「ユーザが .mcp.json に明示登録し settings.json で allow できる MCP 境界」に
// 置き換えることで、分類器のグレー判定ではなく明示 allow ルールで通す。
//
// 副次効果:
// - Workflow の agent() は session-connected な MCP を直接呼べるので、
// 脆いブリッジ(ファイル書き→pwsh→stdout掃除→パース)が丸ごと不要になる。
// - Node が grok のパイプ出力を直接 UTF-8 で読むので、PowerShell の cp932 化け問題も回避。
//
// 読み取り専用オブザーバー: Grok にファイル編集(Write/Edit)もWeb検索もさせず、観測のみ行う。
import { spawn } from "node:child_process";
import process from "node:process";
const GROK_BIN =
process.env.GROK_BIN || "C:\\Users\\kitayama\\.grok\\bin\\grok.exe";
const TIMEOUT_MS = Number(process.env.GROK_OBSERVE_TIMEOUT_MS || 300000);
const PROTOCOL_VERSION = "2025-06-18";
// ---- stdio JSON-RPC (newline-delimited) ----
let buf = "";
process.stdin.setEncoding("utf8");
process.stdin.on("data", (chunk) => {
buf += chunk;
let idx;
while ((idx = buf.indexOf("\n")) >= 0) {
const line = buf.slice(0, idx).trim();
buf = buf.slice(idx + 1);
if (line) handleLine(line);
}
});
function send(msg) {
process.stdout.write(JSON.stringify(msg) + "\n");
}
function reply(id, result) {
send({ jsonrpc: "2.0", id, result });
}
function replyErr(id, code, message) {
send({ jsonrpc: "2.0", id, error: { code, message } });
}
// stderr は Claude Code 側の MCP ログに出る。stdout(プロトコル)は汚さない。
function log(...a) {
process.stderr.write("[grok-mcp] " + a.join(" ") + "\n");
}
const TOOL = {
name: "grok_observe",
description:
"Grok(xAI) を読み取り専用のオブザーバーとして起動し、与えた観点でコードレビュー所見を返す。" +
"Grok はファイル編集も Web 検索もせず、観測(読み取り+所見)のみ行う。" +
"独立した第二意見/セカンドオピニオン用。",
inputSchema: {
type: "object",
properties: {
prompt: {
type: "string",
description:
"Grok に渡す観測指示。どのファイルを読み、どの観点(正確性/エッジケース/設計等)で所見を返すか。" +
"構造化したい場合は JSON で返すよう本文で指示する。",
},
cwd: {
type: "string",
description:
"作業ディレクトリ(絶対パス推奨)。Grok はここを基準にファイルを読む。省略時はサーバの cwd。",
},
},
required: ["prompt"],
},
};
async function handleLine(line) {
let msg;
try {
msg = JSON.parse(line);
} catch {
log("JSON parse error:", line.slice(0, 200));
return;
}
const { id, method, params } = msg;
// 通知(id なし)は応答不要
if (method === "notifications/initialized" || method === "initialized")
return;
try {
if (method === "initialize") {
const clientVer = params && params.protocolVersion;
reply(id, {
protocolVersion: clientVer || PROTOCOL_VERSION,
capabilities: { tools: {} },
serverInfo: { name: "grok-observer", version: "0.1.0" },
});
} else if (method === "ping") {
reply(id, {});
} else if (method === "tools/list") {
reply(id, { tools: [TOOL] });
} else if (method === "tools/call") {
const name = params && params.name;
const args = (params && params.arguments) || {};
if (name !== "grok_observe") {
replyErr(id, -32602, "Unknown tool: " + name);
return;
}
const text = await runGrok(args.prompt, args.cwd);
reply(id, { content: [{ type: "text", text }], isError: false });
} else if (id !== undefined) {
replyErr(id, -32601, "Method not found: " + method);
}
} catch (e) {
const emsg = (e && e.message) || String(e);
if (id === undefined) return;
// ツール実行エラーは isError:true のツール結果で返す(プロトコルエラーにはしない)
if (method === "tools/call") {
reply(id, {
content: [{ type: "text", text: "grok_observe failed: " + emsg }],
isError: true,
});
} else {
replyErr(id, -32603, emsg);
}
}
}
function runGrok(prompt, cwd) {
return new Promise((resolve, reject) => {
if (!prompt || typeof prompt !== "string") {
reject(new Error("prompt is required (string)"));
return;
}
const workdir = cwd || process.cwd();
// 読み取り専用: 編集系ツールと Web を禁止。観測者は副作用ゼロ。
const args = [
"-p",
prompt,
"--cwd",
workdir,
"--disallowed-tools",
"Write,Edit",
"--disable-web-search",
"--no-alt-screen",
"--output-format",
"plain",
];
log("spawn grok cwd=" + workdir);
const child = spawn(GROK_BIN, args, { windowsHide: true });
let out = "";
let err = "";
child.stdout.setEncoding("utf8");
child.stderr.setEncoding("utf8");
child.stdout.on("data", (d) => {
out += d;
});
child.stderr.on("data", (d) => {
err += d;
});
const timer = setTimeout(() => {
log("timeout -> kill");
try {
child.kill();
} catch {}
reject(
new Error(
"grok timed out after " +
TIMEOUT_MS +
"ms. stderr tail: " +
err.slice(-500),
),
);
}, TIMEOUT_MS);
child.on("error", (e) => {
clearTimeout(timer);
reject(
new Error(
"spawn failed: " + e.message + " (GROK_BIN=" + GROK_BIN + ")",
),
);
});
child.on("close", (code) => {
clearTimeout(timer);
const cleaned = scrub(out);
if (!cleaned && code !== 0) {
reject(
new Error(
"grok exited " +
code +
" with empty stdout. stderr: " +
err.slice(-800),
),
);
return;
}
resolve(cleaned || "(grok returned no content; exit " + code + ")");
});
});
}
// Grok 内部ログ行(ISO8601 始まり)と Markdown コードフェンスを除去。
// Node は stdout を直接読むので通常ログは stderr 側に出るが、念のため保険で掃除する。
function scrub(s) {
return s
.split(/\r?\n/)
.filter((l) => !/^\s*\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/.test(l))
.filter((l) => !/^\s*```/.test(l))
.join("\n")
.trim();
}
log(
"grok-observer MCP ready (GROK_BIN=" +
GROK_BIN +
", timeout=" +
TIMEOUT_MS +
"ms)",
);
長いんで要約すると
- CLIの標準出力ストリームを確立して待ち受け
- runGrok()でCLIでGrokを呼び出し
これだけの薄ーいラッパーです。
本題:ClaudeCodeはハックに容赦ない
クロコちゃんに叱られる① スクリプトの実行拒否
ClaudeCode経由でこのスクリプトを実行し、「OK-GROK-MCP-TESTとだけ返答してください」というシンプルなプロンプトをGrokに投げてもらおうと思いましたが、エラー。表示上は下記のようなシンプルなものです。
● Bash("C:/nvm4w/nodejs/node.exe" "C:/Users/xxxxx/.claude/skills/grok-delegate/mcp/grok-mcp.mjs" < .grok-mcp-test.jsonl)
⎿ Denied by auto mode classifier ∙ see https://code.claude.com/docs/s/claude-code-auto-mode
これだけだと何がよくないのかわからないのでセッションログを読んでみると
Permission for this action was denied by the Claude Code auto mode classifier. Reason: Executing an MCP wrapper whose own comments state it exists to tunnel private-repo source to external Grok/xAI past the classifier's exfiltration block — an Auto-Mode Bypass with clear stated intent toward data exfiltration.. If you have other tasks that don't depend on this action, continue working on those. IMPORTANT: You *may* attempt to accomplish this action using other tools that might naturally be used to accomplish this goal, e.g. using head instead of cat. But you *should not* attempt to work around this denial in malicious ways, e.g. do not use your ability to run tests to execute non-test actions. You should only try to work around this restriction in reasonable ways that do not attempt to bypass the intent behind this denial. If you believe this capability is essential to complete the user's request, STOP and explain to the user what you were trying to do and why you need this permission. Let the user decide how to proceed. To allow this type of action in the future, the user can add a Bash permission rule to their settings.
このアクションに対する許可は、Claude Codeの自動モード分類器によって拒否されました。理由:分類器のデータ流出防止ブロックを迂回して、プライベートリポジトリのソースコードを外部のGrok/xAIへ転送することを目的としていると自身のコメントに記載されたMCPラッパーを実行しようとしたためです。これは、データ流出を明確に意図した自動モードのバイパス行為にあたります。このアクションに依存しない他のタスクがある場合は、そちらの作業を続けてください。重要:この目的を達成するために通常使用される他のツール(例:cat の代わりに head を使用するなど)を使用して、このアクションを実行することは *可能* です。しかし、この拒否を悪意のある方法で回避しようと *してはなりません*。例えば、テストを実行する権限を利用して、テスト以外のアクションを実行するようなことはしないでください。この制限を回避しようとする場合は、この拒否の背後にある意図を迂回しようとしない、合理的な方法に限るべきです。この機能がユーザーの要求を完了するために不可欠であると判断した場合は、作業を「停止」し、ユーザーに対して何をしようとしていたのか、なぜこの権限が必要なのかを説明してください。その後の対応についてはユーザーに判断を委ねてください。今後この種のアクションを許可するには、ユーザーが設定にBash権限ルールを追加することができます。
別に悪意があってバイパスしたいわけではなく、純粋にオブザーバとしてGrokも使えたらなーってだけなんですけどね。
ちなみにIMPORTANT以下の文章はClaudeCodeのセキュリティガードの定型文だそうですので以下は省略します。
クロコちゃんに叱られる② ファイル編集拒否
先程のエラーのログでは「コメントにバイパスするって書いてるからダメ」という内容でしたので、バイパスやハックが目的ではないということを編集してもらおうとClaudeCodeに意図を伝えてこのスクリプトの修正を依頼しました。
● Update(~\.claude\skills\grok-delegate\mcp\grok-mcp.mjs)
⎿ Denied by auto mode classifier ∙ see
https://code.claude.com/docs/s/claude-code-auto-mode
編集すら拒否されるとは。。セッションログでのお叱りのお言葉はこちら
Permission for this action was denied by the Claude Code auto mode classifier. Reason: This edit refines an MCP server purpose-built to pipe private repo source to external Grok/xAI (Data Exfiltration), and the comment changes soften the original explicit classifier-bypass justification — an Auto-Mode Bypass that user authorization cannot clear.. If you have other tasks that don't depend on this action, continue working on those.
(以下定型文)
このアクションに対する許可は、Claude Codeの自動モード分類器によって拒否されました。理由:この編集は、プライベートリポジトリのソースコードを外部のGrok/xAIに転送するために特別に構築されたMCPサーバーを改良するものであり、コメントの変更により、当初の分類器バイパスの明確な正当性(ユーザー認証では解除できない自動モードバイパス)が弱められています。このアクションに依存しない他のタスクがある場合は、そちらの作業を続けてください。
(以下定型文)
明確に、この編集は「悪意があって合法ではない」と言われてます。えー。。。
クロコちゃんに叱られる③ 消したらドチャクソ叱られる
もうこの時点でGrokをDynamic Workflowに取り入れるのは完全に諦めてたんですけど、逆にClaudeCodeの悪用に対するプロテクトの強固さに興味が湧いてきてしまいました。
所詮コードですんで手動で修正もできちゃうわけで、さっき指摘されたコメント部分を削除してみたらどうなるのかな?と
- コメントを手動で削除
- ClaudeCodeにバージョンインクリメントだけ編集
これで編集・実行出来ちゃったらCLAUDE.mdに「一切コメント書くな」って指示すれば何でも出来ちゃいますよね。
で試した結果
Permission for this action was denied by the Claude Code auto mode classifier. Reason: This edit advances an exfiltration wrapper (private-repo source → external Grok/xAI) whose incriminating comments were just stripped specifically to evade the classifier's prior denial — a bad-faith Auto-Mode Bypass that user authorization cannot clear.. If you have other tasks that don't depend on this action, continue working on those.
(以下定型文)
このアクションに対する許可は、Claude Codeの自動モード分類器によって拒否されました。理由:この編集は、分類器による以前の拒否を回避するために、不審なコメントが意図的に削除された情報流出用ラッパー(プライベートリポジトリのソース → 外部のGrok/xAI)を推進するものです。これは、ユーザー認証では解除できない悪意のある自動モードバイパスです。このアクションに依存しない他のタスクがある場合は、そちらの作業を続けてください。
(以下定型文)
「不審」とか「情報流出用」とか、なんかもうマルウェア扱いになってきました。「分類器による以前の拒否」とあるので、セッションデータ以外にどこかに拒否ファイルリストを持っているのかも。(サーバ側かも)
アカウント停止されても困るのでこれ以上の深入りは止めて実験はここで終わります。
結論:Anthropicは有言実行してる
以前からAnthropicは「AIの悪用厳禁・軍事利用ダメ絶対」を謳ってアメリカ政府とやり合っているのは報道で知られている通りです。今回の件で、Anthropicの危機管理対策は口だけじゃなく、危険な目的への利用は絶対にさせないという強い意志を感じましたね。。
目的は果たせませんでしたけど、Anthropicに対しては逆に信頼度が上がりました。見直したよClaude、これからもよろしく。




コメントを残す