この記事は16968文字43で読めます

先日、共著で書かせていただいた『AIエージェント開発 / 運用入門』がITエンジニア本大賞2026の技術書部門ベスト10に選ばれまして、翔泳社主催の「技術書の話をしよう。~特集!ITエンジニア本大賞2026~」で登壇してきました。

そのなかで偉そうに自分専用のAIエージェントを作って業務効率化しようという話をしたのですが、話した側の自分自身がある壁にぶつかっていたんですよね…。

#Table of Contents

#はじめに

AIエージェント、便利ですよね。Claude Codeをはじめとするコーディングエージェントの恩恵を日々受けていますし、このブログの執筆にも音声入力とClaude Codeを組み合わせたワークフローを活用しています。

もちろんClaude Code単体でもMCPやブラウザ操作を組み合わせれば、経費精算やメール確認といった事務作業はこなせますし、その場合はサブスクリプションの範囲内で動くのでコストの心配はありません。

しかし、自分専用のUI/UXを持ったAIエージェントアプリとして作ろうとすると話が変わってきます。通常、独自アプリからLLMを利用する場合はAPIキーの従量課金になるので、エージェントをブンブン回すと冗談抜きで月の請求が恐ろしいことになるわけです。

で、この問題に対して1つの解を見つけました。Claude CodeのサブスクリプションをバックエンドにしたパーソナルAIエージェント「むぎ苦労mugi-claw)」です。名前は飼い犬のむぎから取りつつ、OpenClawへのオマージュと、文字通り苦労して作ったという意味を込めています。

むぎ苦労の動作

この記事では、mugi-clawの技術的な実装詳細を中心に紹介していきます。

#LLMの利用コストとサブスクリプション

#API従量課金というつらみ

まず、なぜサブスクリプションにこだわったかという話です。

LLMのAPIは従量課金、つまり消費したトークン数に応じて課金されます。Claude Opus 4.6の場合、入力が $5/100万トークン、出力が $25/100万トークンです(Anthropic公式価格表より)。

一見すると安く感じるかもしれませんが、Claude Codeのようなコーディングエージェントは単純なチャットとは桁違いにトークンを消費します。

エージェントループのたびにファイルを読み込み、コンテキストを組み立て、ツールを実行し、その結果をまた読み込む。1回のやりとりで5万〜15万トークンの入力が飛ぶのは珍しくありません。

Anthropicの公式ドキュメントでは、APIキー利用時の平均コストは1日あたり約 $6(約900円) で、90%のユーザーが $12/日以下に収まるとされています(Manage costs effectivelyより)。ただしこれはSonnet中心のライトな使い方での平均値です。皆さまもこんなはず無いよな〜とこのドキュメントを見て思ったのではないでしょうか。

Opus 4.6をメインに、日勤帯(8時間)でがっつりコーディングに使った場合のとある日の私の利用状況はこうなりました。

項目トークン数単価コスト
入力(非キャッシュ 約5%)約500K$5/MTok$2.50
キャッシュ書き込み約1M$6.25/MTok$6.25
キャッシュ読み取り(約95%)約19M$0.50/MTok$9.50
出力約1M$25/MTok$25.00
1日合計約 $43(約6,500円)

キャッシュヒット率が高いおかげで入力側はだいぶ助かりますが、出力トークンの単価が重いです。大規模リファクタリングなどヘビーに使うと1日 $80〜120(約12,000〜18,000円) に達することもあります。

これを月22営業日で計算すると、月額 $950〜2,640(約14万〜40万円) です。さすがにつらいですね、これは。貧乏人にはきつすぎます。

#Claude Codeのサブスクリプション

一方で、Claude Codeにはサブスクリプションプランがあります。

プラン月額Proとの使用量比
Pro$201x(基準)
Max 5x$1005倍
Max 20x$20020倍

Max 20xのAPI換算価値は月$2,000超とする分析もあります(ksred.comの8ヶ月追跡データより)。月$100〜200でこれだけの使用量が得られるなら、サブスクリプションを活用しない手はないですよね。

ちなみに執筆時点(2026年3月27日)では、Claude March 2026 Usage Promotionという期間限定キャンペーン(3/13〜3/28)が実施されており、オフピーク時間帯の使用量が2倍になっていました。日本時間だと午前3時〜午後9時がオフピーク扱いなので、日中の業務時間がまるごと2倍。Max 20xならさらに余裕があり、Opus 4.6を1日中使ってもなかなか上限に達しない状況でした。

ここまで来ると、コーディング以外のこともClaude Codeにお願いしようかなという気になってくるわけです

#OpenClawという先行事例と教訓

サブスクリプションを活用してチャットサービス上でAIエージェントを動かすというアイデア自体は、OpenClawが先行していました。OpenClawはWhatsApp、Discord、Slackなど多数のチャットプラットフォームに対応したオープンソースのAIエージェントで、GitHub上で339kスター以上を獲得しています(GitHubリポジトリより)。

しかし、OpenClawにはいくつかの深刻な問題がありました。

まずセキュリティです。2026年初頭から複数のCVEが報告されており、WebSocketハイジャックによるワンクリックRCE(CVE-2026-25253、CVSS 8.8)、コマンドインジェクション(CVE-2026-24763)、プロンプトインジェクション経由のコード実行(CVE-2026-30741)など、かなり重大な脆弱性が見つかっています(Sangforのセキュリティレポートより)。

さらにスキルマーケットプレイスであるClawHubでは、Snykの調査で3,984スキル中1,467個にセキュリティ上の問題が発見され(うち76個が確認済みのマルウェア)、なかには34万インストールを超える規模でクレデンシャルを窃取していたスキルもあったという報告があります(Blink Blogより)。

そして利用規約の問題です。OpenClawはClaude Pro/MaxのOAuthトークンを流用してAPIアクセスする、いわゆるサブスクリプションハックに対応していましたが、Anthropicは2026年1月にサーバー側でOAuthトークンの利用を公式クライアントのみに制限する技術的措置を実施しました(GitHub Issue #559より)。アカウントBANの事例も報告されています。

OpenClawのアプローチ自体には共感する部分がありますが、セキュリティと利用規約の両面で諸刃の剣だなというのが正直な感想です。mugi-clawを作るにあたっては、この教訓を踏まえた設計を心がけました。

#mugi-clawの概要

#なぜSlackなのか

mugi-clawはSlack専用のClaude Code駆動パーソナルAIエージェントです。コードは公開していますが、あくまで自分用にチューニングしたものなので、参考程度に見ていただければと思います。

さて、Claude Codeでできることを他のアプリから動かすだけなら、別にSlackである必要はないですよね。ターミナルから直接やればいい話です。

でも、PCを開いていないときに限って事務作業をやりたくなりません?

自分の場合、PCに向かっている時間はコーディングに集中したい。経費精算やメール確認みたいなめんどくさい作業は、通勤中や隙間時間にスマートフォンからちょいちょいで片付けたいんですよね。スマートフォンにはSlackが入っている。そこからAIエージェントに指示を出せれば、PCを開かなくても色々なことが捗るわけですよ…。

これがSlackにした理由です。

#利用規約の確認

「サブスクリプションのClaude Code CLIを別のアプリから呼び出すのって、利用規約的に大丈夫なの?」と思う方もいるでしょう。自分も気になったので、関連するドキュメントと規約を読み込んで整理しました。

まず、Claude Code CLIの -p フラグによる自動化は公式のヘッドレスモードドキュメントで明確に想定されたユースケースです。ログ分析、CI/CDでの翻訳自動化、セキュリティレビューなどの例が紹介されており、スクリプトやパイプラインから claude -p を呼び出す使い方は公式に案内されています。

次に、Legal and complianceのページでは、OAuthの認証情報について以下のように記載されています。

OAuth authentication (used with Free, Pro, and Max plans) is intended exclusively for Claude Code and Claude.ai. Using OAuth tokens obtained through Claude Free, Pro, or Max accounts in any other product, tool, or service — including the Agent SDK — is not permitted and constitutes a violation of the Consumer Terms of Service.

ここで禁止されているのは、OAuthトークンを抽出して別のプロダクトやサービスで使うことです。mugi-clawはOAuthトークンを取り出しているわけではなく、Claude Code CLIそのものを child_process.spawn() で起動しています。CLIプロセスの内部でOAuth認証が行なわれる構造であり、OpenClawのようにトークンを流用して独自のAPIクライアントからアクセスするのとは異なります。

ただし、これは明確にセーフとまでは言い切れません。 mugi-clawはSlackアプリとしてClaude Code CLIをラップしている構造であり、見方によっては別のプロダクトとしてClaude Codeを提供していると解釈される余地はあります。現時点ではCLIの自動化ユースケースが公式に許可されているため、自分のマシンで自分だけが使う範囲では問題ないと考えていますが、Anthropicの解釈次第ではグレーになりうる領域です。

さらに重要なのが、Consumer Terms of Serviceのアカウント共有禁止条項です。

You may not share your Account login information, Anthropic API key, or Account credentials with anyone else. You also may not make your Account available to anyone else.

自動アクセスについても、原則禁止のうえでClaude Codeなどの公式ツールを例外とする構造になっています。

Except when you are accessing our Services via an Anthropic API Key or where we otherwise explicitly permit it, to access the Services through automated or non-human means, whether through a bot, script, or otherwise.

mugi-clawはSlack上で動くため、ワークスペースに入っている他のメンバーが1人のサブスクリプションを使ってAIにアクセスできてしまう構造になっています。これは上記のアカウント共有禁止条項に明確に抵触します。

利用する場合は個人のワークスペースでのみ運用し、複数人のユーザーが使えないようにしてください。利用規約の解釈が変わる可能性もあるため、最新の規約を定期的に確認することをおすすめします。むぎぼーとのお約束ですよ。

#全体アーキテクチャ

mugi-clawの全体像を図にすると以下のようになります。

@mention / スケジュール

spawn -p --output-format stream-json

NDJSON ストリーム

結果・進捗・承認ボタン

ユーザー(Slack)

mugi-claw

Node.js アプリケーション

Claude Code CLI

sandbox-exec 内で実行

MCP サーバー群

ブラウザ / デスクトップ / モバイル

外部サービス

Chrome CDP / Android Emulator

セキュリティ層

プロキシ + Hooks + Credential Server

TypeScriptで書かれたNode.jsアプリケーションが、Slack BoltSocket ModeでSlackに常駐します。ユーザーからメンションを受けると、Claude Code CLIを child_process.spawn() で起動し、NDJSONストリームをリアルタイムにパースしてSlackスレッドに進捗を投稿するという流れです。

ブラウザ操作、デスクトップ操作、モバイル操作はそれぞれMCP(Model Context Protocol)サーバーとして実装されており、Claude Code CLIから呼び出されます。セキュリティはmacOSの sandbox-exec とプロキシの2層で担保しています。

また、LLMOpsの観点ではLangfuseによるトレーシングも設定しています。Claude CodeのPreToolUse/PostToolUseフックからLangfuse Ingestion APIにデータを送信する仕組みで、詳しい実装は以前の記事で紹介しました。mugi-clawでも同じ仕組みを使って、エージェントのツール実行やトークン消費を可視化しています。

#Claude Code CLIのヘッドレス実行

ここからは実装の詳細に入ります。mugi-clawの心臓部は、Claude Code CLIをヘッドレス(非インタラクティブ)モードで spawn する部分です。

#spawnと主要フラグ

Claude Code CLIには -p--print)フラグがあり、標準入力を介さずプロンプトを直接渡して1回のリクエストを処理できます。これにストリーム出力の --output-format stream-json を組み合わせると、リアルタイムにNDJSON形式でイベントが流れてきます。

const args = [
  '-p', prompt,
  '--output-format', 'stream-json',
  '--max-turns', String(this.config.claude.maxTurns),
  '--verbose',
];

// 使用を許可するツールを明示的に指定
args.push('--allowedTools', ...tools);

// MCP サーバー設定
args.push('--mcp-config', '.mcp.json');

// スレッド内の再メンション時はセッションを継続
if (resumeSessionId) {
  args.push('--resume', resumeSessionId);
}

// モデル指定(Slack Home画面から切り替え可能)
if (model) {
  args.push('--model', modelId);
}

--allowedTools には、Claude Code内蔵ツール(ReadWriteBashWebSearch 等)に加えて、自作MCPのツール群(mcp__browser__browser_navigate 等)を指定しています。Gmail操作やGoogleカレンダー操作などの定型タスクは .claude/skills/ にスキルとして定義しており、Claude Codeが必要に応じて自動で呼び出します。

サンドボックスが有効な場合は、macOSの sandbox-exec でCLIプロセスをラップします。

const command = this.config.sandbox.enabled
  ? 'sandbox-exec'
  : this.config.claude.cliPath;

const spawnArgs = this.config.sandbox.enabled
  ? ['-f', this.config.sandbox.profile, this.config.claude.cliPath, ...args]
  : args;

// プロキシ環境変数を注入
const proxyEnv = this.config.sandbox.enabled
  ? {
      HTTP_PROXY: `http://localhost:${proxyPort}`,
      HTTPS_PROXY: `http://localhost:${proxyPort}`,
    }
  : {};

const child = spawn(command, spawnArgs, {
  env: {
    ...process.env,
    ...proxyEnv,
    MUGI_CLAW_APPROVAL: '1',
    APPROVAL_PORT: String(this.config.approval.port),
    APPROVAL_CHANNEL: approvalContext.channel,
    APPROVAL_THREAD_TS: approvalContext.threadTs,
  },
  stdio: ['ignore', 'pipe', 'pipe'],
});

sandbox-exec -f mugi-claw.sb claude -p ... という形でCLIが起動され、環境変数でHTTPプロキシのアドレスと承認サーバーの接続情報を渡しています。

#NDJSONストリームのパース

--output-format stream-json を指定すると、Claude Code CLIは処理の進行に応じてNDJSON(1行1JSON)でイベントを出力します。現時点のCLI出力では system_initassistanttool_usetool_resultresult などのイベントタイプがあり、これをリアルタイムにパースしてSlackスレッドの進捗表示に反映させています。なお、 --output-format stream-json 自体は公式ドキュメントに記載されていますが、個々のイベントタイプの詳細な仕様は明示されていないため、CLIのアップデートで変更される可能性があります。

for await (const line of readLines(child.stdout)) {
  if (!line.trim()) continue;
  const event = JSON.parse(line);

  switch (event.type) {
    case 'system_init':
      // セッションIDを保存(--resume用)
      sessionManager.save(threadTs, event.session_id);
      break;
    case 'assistant':
      // テキスト応答 → Slackスレッドに進捗表示
      threadManager.updateStatus(event.content);
      break;
    case 'tool_use':
      // ツール実行中の表示(ツール名に応じた絵文字付き)
      threadManager.showToolExecution(event.tool, event.input);
      break;
    // (中略)
  }
}

#セッション継続と同時実行制御

Slackのスレッド形式を活かして、同一スレッド内で再メンションすると前回のセッションを引き継げるようにしています。 system_init イベントから取得した session_idMap<threadTs, ClaudeSession> で保持し、再メンション時に --resume フラグで渡す仕組みです。24時間以上経過した古いセッションは自動でクリーンアップされます。

同時実行制御としては、デフォルト最大3並列で動作し、超過分はキューに入って指数バックオフリトライ(2秒、4秒、8秒、最大3回)で処理されます。個人でしか使わないので、正直3並列でも十分すぎるくらいです。

#Slack連携の実装

#スレッド形式のリアルタイム進捗

Slackとの接続はSocket Modeを使っています。HTTPエンドポイントの公開が不要で、自宅のMacBookからでもWebSocketで直接つながるのが都合がいいです。

app_mention イベントを受信すると、まず「考え中わん…」というステータスメッセージをスレッドに投稿し、そこをリアルタイムに更新していきます。

Slackでmugi-clawにメンションすると「考え中わん...」のステータスが表示される

ツール実行中はツール名に応じた絵文字を表示し、いま何をやっているか一目でわかるようにしています。

WebSearchツール実行中の進捗表示

ステータスメッセージの更新はSlack APIのレート制限を考慮して2秒間隔のデバウンスをかけています。完了時の結果メッセージは4,000文字を超える場合に自動分割して投稿します。

#承認/拒否ボタン

Slack上で動かす以上、すべてのツール実行を自動承認にすると怖いですよね。一方で、承認を求めすぎるとSlack上でボタンを連打する羽目になり、体験が最悪になります。

mugi-clawでは、Claude CodeのPreToolUseフックでツールの種類ごとに3段階の判定を行ないます。

# 読み取り系 → 自動承認(exit 0)
Read|Glob|Grep|WebSearch|WebFetch|browser_screenshot|browser_get_text)
  exit 0 ;;

# 書き込み系 → Slackで承認を求める(exit 1)
Write|Edit)
  # Approval ServerにHTTPリクエストを送り、Slackにボタンを投稿
  curl -s "http://localhost:${APPROVAL_PORT}/api/request-approval" \
    -d "{\"tool\":\"${TOOL_NAME}\",\"channel\":\"${APPROVAL_CHANNEL}\"}"
  # (中略:レスポンスのapproved/deniedを判定)
  ;;

# 危険なBashコマンド → 承認必要
*rm*|*sudo*|*git push*|*launchctl*)
  # 同上の承認フロー
  ;;

承認ボタンはBlock Kitactions ブロックで実装しており、 OWNER_SLACK_USER_ID に設定されたユーザーのみが操作できます。10分間応答がなければ自動で拒否されます。

ツール実行時の承認/拒否ボタン。ツール名と入力パラメータが表示され、承認か拒否を選択できる

#ブラウザ操作とコンピュータユース

#Chrome DevTools Protocolによる接続

mugi-clawの大きな特徴の1つが、Chrome DevTools Protocol(CDP)を使ったブラウザ操作です。

あらかじめChromeを --remote-debugging-port=9222 オプション付きで起動しておき、Playwrightchromium.connectOverCDP() で接続します。

const browser = await chromium.connectOverCDP(wsUrl);
const context = browser.contexts()[0];
// MCPプロセスごとに専用のタブを作成(複数スレッドの同時操作に対応)
const page = await context.newPage();

なぜCDPにこだわるかというと、ログイン済みのセッションをそのまま使いたいからです。

GmailやGoogleカレンダー、X(旧Twitter)など、普段ブラウザでログインして使っているサービスを、AIエージェントにも同じセッションで操作させたいものです。

もちろんGoogle Workspace向けのMCPサーバーを使えばAPIキーでアクセスする方法もありますが、Google CloudでOAuth設定を作ってAPIキーを発行して…というのが実業務ではサービスの管理者に申請したり、権限を意識しながらドキュメントを見ながらなれない操作をするのが意外とハードルが高いです。CDPなら、Chromeでログインさえしていれば、人間がやることと同じ操作をAIに任せられます。

トークン効率は正直非常に悪いです。毎回スクリーンショットを撮ったりDOMを解析したりと手間がかかるので、API直接叩くより何倍も遅いです。でも、APIキーを発行できないサービスや、そもそもAPIが公開されていないサービスでも操作できるという汎用性には代えられません。

#モバイル操作

ブラウザだけでなく、ネイティブアプリしかないサービスにも対応するために、Androidエミュレータの操作もサポートしています。ベースは @mobilenext/mobile-mcp を使い、これに不足している機能を自作の mobile_extra MCPで補っています。

{
  "mcpServers": {
    "browser": { "command": "node", "args": ["dist/browser/mcp-server.js"] },
    "desktop": { "command": "node", "args": ["dist/desktop/mcp-server.js"] },
    "mobile":  { "command": "npx", "args": ["-y", "@mobilenext/mobile-mcp@latest"] },
    "mobile_extra": { "command": "node", "args": ["dist/mobile/mcp-server.js"] }
  }
}

自作部分は3つの機能を提供しています。 mobile_screenshot_slack はスクリーンショットをSlackスレッドに自動投稿する機能で、サードパーティの mobile_take_screenshot にはSlack連携がないため、PreToolUseフックで拒否してこちらに誘導しています。 mobile_secure_input後述する機密情報入力の仕組みのモバイル版で、入力値を adb shell input text でエミュレータに送信します。残りはエミュレータのライフサイクル管理(起動・停止・一覧)です。

#スクリーンショットのSlack連携

ブラウザ操作中のスクリーンショットは、自動でSlackスレッドにアップロードされます。Approval Serverの /api/upload-screenshot エンドポイントにPOSTし、Slackの filesUploadV2 でスレッドに投稿する仕組みです。

ブラウザでGmailを操作した結果がスクリーンショット付きでSlackスレッドに投稿されている様子

スマートフォンからSlackを見ているだけで、AIが今どの画面を操作しているのか視覚的に確認できるのはなかなか便利です。

#機密情報をLLMに渡さない仕組み

ブラウザ操作をさせるとなると、どうしてもログイン画面に遭遇します。セッションが切れていたり、新しいサービスにはじめてアクセスしたり。そのときにパスワードやOTPをLLMのコンテキストに載せるわけにはいきません。

mugi-clawでは、 browser_secure_input というMCPツールで機密情報をLLMに直接渡さずにブラウザのフォームに入力する仕組みを実装しています。

Chromeユーザー(スマホ)SlackCredential Server :3457Credential ManagerBrowser MCPClaude CodeChromeユーザー(スマホ)SlackCredential Server :3457Credential ManagerBrowser MCPClaude Codevalues = null で即時GC対象にbrowser_secure_input(selector, site)POST /api/credential-request「機密情報の入力が必要です」(Web UIリンク付き)スマホからフォームにアクセスパスワード入力フォーム表示パスワード入力・送信Promise resolve(values)values返却page.fill(selector, value)"Secure input completed: 1 field(s) filled"

3つのコンポーネントが連携して動きます。

Browser MCPが browser_secure_input ツールの呼び出しを受けると、Approval Serverの /api/credential-request にブロッキングHTTP POSTを送ります。Credential Managerはローカルネットワーク上のIPアドレスを自動取得して http://{local-ip}:3457/credential/{requestId} というURLを生成し、SlackにBlock Kitメッセージとしてリンクを投稿します。

このURLはローカルIPなので、アクセスできるのはmugi-clawが動いているMacBookと同一LANにいる場合か、VPN経由で自宅ネットワークに接続している場合に限られます。機密情報の入力フォームがインターネットに公開されないという点で、露出範囲を限定する防御ラインにはなっています。

ただし、ローカルIPだから安全というわけではありません。同一LAN内の攻撃者からはアクセス可能ですし、より深刻なのはDNSリバインディング攻撃です。攻撃者が用意した悪意あるWebページにアクセスすると、DNSの応答を操作してブラウザからローカルIPのCredential Serverにリクエストを送ることが可能になります(OpenClaw自体にも同様の脆弱性が報告されています)。Hostヘッダー、Sec-Fetch-Site、Originの検証やCSRFトークンの導入を行ない、この手の対策は進めているものの現時点では開発者としてまだ不安があります。

ユーザーはスマートフォン等からそのURLにアクセスし、HTMLフォームにパスワードを入力して送信します。フォームのパスワードフィールドは type="password" かつ autocomplete="off" で、5分のカウントダウンタイマー付きです。送信された値はCredential ManagerのPromiseをresolveし、Browser MCPに返されます。

ここが重要なポイントですが、Browser MCPは受け取った値で page.fill(selector, value) を実行した後、即座に values = null として参照を切断しています。LLMには Secure input completed: 1 field(s) filled on example.com というメッセージだけが返り、クレデンシャルの中身はコンテキストに一切載りません。

ログにも出力しません。

ただし正直に言うと、この仕組みには穴があります。悪意のあるスキルやプロンプトインジェクションでパスワード欄の値をDevToolsから読み取って外部に送信しろと指示されれば、原理的には突破できてしまいます。このあたりのハードニングは今後の課題です。

#セキュリティ対策

#macOS sandbox-execによる制限

Claude Code CLIにはサンドボックス機能が組み込まれていますが、mugi-clawでは独自のサンドボックスで動かしています。macOSのカーネルレベルのサンドボックス機構である sandbox-exec を使い、プロセスのリソースアクセスを制限します。

(version 1)
(allow default)

;; ネットワーク: 外部接続をブロック、localhostのみ許可
(deny network-outbound (remote ip "*:*"))
(allow network-outbound (remote ip "localhost:*"))
(allow network-outbound (remote tcp "localhost:*"))
(allow network-outbound (remote unix-socket))
(allow network-inbound)
(allow network-outbound (remote udp "*:53"))  ;; DNS

;; ファイルシステム: 機密パスをブロック
(deny file-read* (subpath "/Users/tubone24/.ssh"))
(deny file-read* (subpath "/Users/tubone24/.aws"))
(deny file-read* (subpath "/Users/tubone24/.gnupg"))
;; (中略:書き込みも同様にブロック)

設計判断として (deny default) ではなく (allow default) を採用しています。Claude Code CLIは複雑なバイナリで、 deny default だと必要な低レベルシステム操作をすべて列挙する必要があり非現実的でした。代わりに、ネットワークと機密ファイルのみをピンポイントでdenyする方針です。

なお、 sandbox-exec はAppleにより2016年頃(macOS Sierra)から公式にはdeprecated扱いとされています。ここで少し技術的な背景を補足しておきます。

macOSのサンドボックスは3つのレイヤーで構成されています。最下層にあるのがSeatbeltというカーネルレベルの強制アクセス制御(MAC)フレームワークで、これがサンドボックスの実際の強制を担っています。その上にAppleが開発者向けに提供するApp Sandbox(エンタイトルメントベース、.appバンドル向け)があり、そして sandbox-exec はSeatbeltをコマンドラインから利用するためのCLIラッパーです。

deprecatedなのはこの sandbox-exec(CLIラッパー)だけであり、基盤のSeatbeltカーネル機構自体は現役です。App SandboxもSeatbeltの上に構築されていますし、Google ChromeなどのブラウザもSeatbeltのC API(sandbox_init_with_parameters())を直接呼び出してサンドボックスを実現しています(ただしChrome自体は sandbox-exec コマンドは使わず、低レベルAPIを直接利用しています)。

Appleが sandbox-exec をdeprecatedにした理由は、Seatbeltプロファイルを記述するSBPL(Seatbelt Profile Language)がサードパーティ向けに文書化されていない非公式言語であり、macOSの内部構造に精通した専門家でないと正しいプロファイルを書くのが困難だからです(Apple Developer Forumsより)。つまり、基盤技術が危険だから非推奨にしたのではなく、非公式言語で正しくサンドボックスを構成するのは難しいから、App Sandboxを使ってほしいというユーザビリティ上の判断です。

しかし、App Sandboxは .app バンドルとエンタイトルメントが前提であり、CLIツールやスクリプトには直接使えません。Apple公式のCLI向け代替は提供されておらず、結果として sandbox-exec が事実上の唯一の選択肢として残っています。Claude Code自身も @anthropic-ai/sandbox-runtime パッケージを通じて sandbox-exec と動的生成のSeatbeltプロファイルを使っており、macOS 15.xでも問題なく動作します。mugi-clawも同じく sandbox-exec 経由でSeatbeltを利用しているため、実質的なセキュリティリスクは低いと判断しています。

#プロキシによるホワイトリスト制御

sandbox-exec でlocalhostのみネットワーク接続を許可しているため、外部へのHTTP/HTTPSアクセスはすべて内部プロキシ(ポート18080)を経由します。ここにホワイトリストを持たせて、許可されたドメインのみ通信を許可する仕組みです。

ホワイトリストは3層構造になっています。デフォルト(環境変数 DEFAULT_WHITELIST から読み込み、 *.googleapis.com のようなワイルドカード対応)、一時許可(インメモリ、再起動でクリア)、永続許可(SQLiteに保存)の3つです。

ホワイトリストにないドメインへのアクセスが発生すると、Slackスレッドに3つのボタン付きメッセージが投稿されます。Allow Onceで一時許可、Allow PermanentlyでSQLiteに永続保存、Denyまたは10分タイムアウトで拒否します。Slackのスマートフォンアプリからホワイトリストを管理できるので、PCを開く必要がありません。

#2層防御の設計思想

sandbox-exec(カーネルレベル)とプロキシ(アプリケーションレベル)の2層で防御しています。

仮にClaude Codeが curl --noproxync でプロキシをバイパスしようとしても、 sandbox-exec がカーネルレベルで外部ネットワーク接続をブロックします。逆に、 sandbox-exec の制御をすり抜ける方法が見つかったとしても、プロキシ側のホワイトリストで未許可のドメインへのアクセスが止まります。

どちらか一方が突破されても、もう一方が防いでくれる。これが2層にしている理由です。

さらにツールレベルでは、PreToolUseフックとガードスクリプトが .env ファイルへのアクセスやプロジェクト外のファイル削除をブロックしています。

ただし、この2層防御はあくまでネットワークとファイルシステムへのアクセスを制限するものです。LLM自体が悪意あるプロンプトインジェクションによって意図しない動作をするケース、たとえば許可済みのドメインに対して機密情報を含むリクエストを送信するようなロジックレベルの攻撃には、サンドボックスもプロキシも効きません。この種のリスクに対しては、スキルを自作のものに限定する運用面での対策と、PreToolUseフックによるツール入力の検査で対応していますが、完璧とは言えません。限界は認識しておく必要があります。

#運用と管理機能

#Slackホーム画面

SlackのApp Homeタブに管理画面を用意しています。

機能内容
プロフィール設定呼び名、場所、タイムゾーン、趣味、興味分野
スケジュール一覧cron形式で定期タスクの登録・編集・削除・一時停止・即時実行
モデル切り替えOpus / Sonnet / Haiku をセレクトボックスで変更
ホワイトリスト管理ドメインの追加・編集・削除
通知設定完了時・エラー時の通知ON/OFF

mugi-clawのApp Home画面。プロフィール、スケジュール、モデル切り替え、ホワイトリスト管理が並ぶ

PCを開かなくても運用に必要な操作はひと通りSlackから行なえます。プロフィール情報は天気予報の取得やニュースのフィルタリングに使われ、スケジュール実行は node-cron で動いています。

#自己デプロイ

ちょっと変わった機能ですが、mugi-clawは /mugiclaw deploy というSlashコマンドで自分自身をデプロイできます。SREの文脈では、デプロイ対象とデプロイシステムが同一である構造は循環依存(Cyclic Dependency) と呼ばれ、デプロイ失敗時にシステムごと復旧手段を失うコールドスタート問題を引き起こすため非推奨とされています(Google SRE Workbook - Simplicityより)。ただ、mugi-clawは個人利用のツールですし、最悪SSHで入って手動復旧すればいいので、ここは利便性を優先しています。

// 1. git pull
await exec('git pull');
notify(dm, 'git pull 完了');

// 2. ビルド
await exec('npm run build');
notify(dm, 'ビルド完了');

// 3. 自分自身を再起動
await exec(`launchctl kickstart -k gui/$(id -u)/com.mugi-claw`);

別のPCでコードを書いてGitHubにpushしておけば、Slackから deploy するだけでpull → build → 再起動が走ります。各ステップの進捗はDMでリアルタイム通知されるため、ビルド失敗時もすぐ気づけます。

#launchdによる常時稼働

macOSの launchd でデーモン化しています。

<key>RunAtLoad</key>
<true/>

<key>KeepAlive</key>
<dict>
    <key>SuccessfulExit</key>
    <false/>
</dict>

<key>ThrottleInterval</key>
<integer>10</integer>

RunAtLoad でログイン時に自動起動、 KeepAliveSuccessfulExit: false で異常終了時に自動再起動。 ThrottleInterval で最低10秒のインターバルを設けて、クラッシュループを防いでいます。

自宅で使っていないMacBookをクラムシェルモードで常時稼働させて、このplistで動かしています。Claude Codeが流行ったときにMac miniを買って常時稼働させる人が多かったみたいですが、自分はMac miniを持っていなかったので余ったMacBookで代用しています。

#現状の課題

正直に言って、まだまだ課題だらけです。

まずブラウザ操作やモバイル操作の速度。毎回スクリーンショットを撮ったりDOMを解析したりするので、1つの操作に数十秒かかることもあります。APIを直接叩くのと比べると圧倒的に遅い。ただ、APIが存在しないサービスにも対応できるという汎用性とのトレードオフなので、ここは割り切っています。

セキュリティの穴も残っています。Credential Serverの入力値を悪意あるプロンプトで読み取られるリスク、プロンプトインジェクションへの耐性、スキルの信頼性検証など、OpenClawの事例を見ても分かるように、このジャンルのアプリケーションはセキュリティが永遠の課題です。

現時点でのワークアラウンドとしては、スキルは外部のマーケットプレイスから取得せず、すべて自作のものだけを使う運用にしています。OpenClawのClawHubで起きたマルウェア問題を考えると、信頼できないスキルを入れないというのが一番シンプルな防御策です。また、ネットワーク経由でのプロンプトインジェクション(悪意あるWebページの隠しテキスト等)については、プロキシ側でレスポンスを検査して危険なパターンをフィルタリングできないか検討しています。

そして安定性。Claude Code CLIのアップデートで挙動が変わったり、Slack APIのレート制限に引っかかったり、CDPの接続が不安定になったり。常時稼働させていると色々起きます。 launchd のおかげで落ちても自動復帰しますが、根本的な安定性の向上は引き続き取り組んでいく必要があります。

#最後に

サブスクリプション駆動のSlack常駐AIエージェント「むぎ苦労(mugi-claw)」を紹介しました。

従量課金の呪いから逃れるためにサブスクリプションを活用し、Chrome DevTools Protocolでログイン済みセッションを再利用し、機密情報はLLMのコンテキストに載せず、sandbox-execとプロキシの2層でセキュリティを確保する。書き出してみると結構な苦労でした。名前の通りですね。

コードは以下に置いてあります。使う場合はくれぐれも利用規約とセキュリティリスクを理解したうえで、自己責任で。

Claude Codeのサブスクリプションで遊ぶネタが尽きない予感がするこの頃です。

tubone24にラーメンを食べさせよう!

ぽちっとな↓

Buy me a ramen