Note: このプロジェクトは Anthropic 非公式のサードパーティツールです。Anthropic による公式サポートの対象外です。
ディレクトリを指定して Claude Code (opencode、GitHub Copilot CLI も可) を起動する Web フロントエンド。 VS Code のようにフォルダを選択し、ブラウザ内のターミナルで操作できます。
ブラウザ (xterm.js) <── WebSocket ──> Fastify <── node-pty ──> claude / opencode / copilot CLI
<── HTTP REST ──> (ディレクトリ一覧 API)
| レイヤー | 技術スタック |
|---|---|
| Frontend | React 19 + Vite + xterm.js |
| Backend | Node.js + Fastify + @fastify/websocket + node-pty |
- Node.js >= 22 / npm >= 9
- C++ コンパイラ(node-pty のビルドに必要。Arch:
base-devel、Ubuntu:build-essential) - Claude Code CLI — このプロジェクトの主対象。既定で起動するエージェント。
- opencode — 任意。入っていれば起動時にアプリとして選べます (下記「起動」参照)。入れずに opencode を選んだ場合、ターミナルに
execvp(3) failed等のエラーが表示され起動に失敗します。 - GitHub Copilot CLI (
copilot) — 任意。インストール:npm i -g @github/copilot(またはインストールスクリプト /brew install copilot-cli/ winget)。入れずに選んだ場合も同様に起動に失敗します。認証は初回/login(OAuth) か環境変数GH_TOKEN/GITHUB_TOKENで行います。
git clone <repo-url> ccserver
cd ccserver
npm installターミナルを 2 つ開いて実行:
# バックエンド (port 3001)
npm run dev:server
# フロントエンド (port 5173)
npm run dev:clientブラウザで http://localhost:5173 を開く。
npm run build --workspace=client
NODE_ENV=production node server/index.jsブラウザで http://localhost:3001 を開く。ポートは環境変数 PORT で変更可能 (PORT=8080 NODE_ENV=production node server/index.js)。
常駐させたい場合は systemd でバックグラウンド実行、Tailnet 内から HTTPS で見たい場合は Tailscale Serve で HTTPS 公開 を参照。
- ディレクトリブラウザでフォルダを選択
- シングルクリック → フォルダ内に移動
- ダブルクリック → そのフォルダで (既定の設定のまま) 起動
- Back ボタン → ディレクトリ選択に戻る
- ブラウザ内ターミナルで操作
「Claude Code」ボタン右の ▼ から開くモーダルで、起動方法を選べます:
| 項目 | 選択肢 | 記憶される場所 |
|---|---|---|
| アプリ | Claude Code / opencode / GitHub Copilot | ブラウザの localStorage (次回以降の既定) |
| 起動モード | 通常起動 / 🔒 サンドボックスで起動 | 同上 |
| GPG署名を使う | on/off (既定 off) | localStorage にディレクトリ単位で |
| ssh-agentを転送する | on/off (既定 off) | 同上 |
サンドボックス・GPG・ssh-agent の詳細は サンドボックス (bwrap + rootless docker) を参照。「アプリ」と「起動モード」のどちらの項目をクリックしても、選んだ内容で即座に起動します。
コンボ起動のロール別アプリ選択 (ワーカーA / ワーカーB / オーケストレーター) もブラウザの localStorage に記憶され、次回のコンボ起動の既定になります (初期値: ワーカーA・オーケストレーターが Claude Code、ワーカーB が opencode)。単発起動の「アプリ」記憶とは独立しており、コンボ起動には defaultApp は適用されません。
新規セッションの既定アプリ・サンドボックス設定は sandbox.config.json (下記「設定ファイル」参照) でサーバー全体の初期値を決められますが、上記モーダルで一度でも明示的に選んだ後はブラウザ側の記憶が優先されます。
サーバーにインストールされていない CLI は選択できません: ccserver は起動モーダル表示時にサーバー側の実行ファイル解決 (PATH・サーバーの node バイナリディレクトリ・~/.local/bin・アプリ別ディレクトリ) を確認し、見つからないアプリはグレーアウトされます (ツールチップ「サーバーに未インストール」)。既定アプリが未インストールの場合も、利用可能なアプリへ自動で切り替えます。何らかの経路で未インストールのアプリが指定された場合 (例: 予約プロンプトの自動再開)、サーバーは Cannot launch: <app> is not installed on this server (searched ...) という明示エラーを返します。インストール/アンインストールした場合はブラウザを再読込すれば反映されます。
opencode を選んだ場合の挙動の違い:
- クリップボード同期 (OSC 52): opencode がターミナルに書き込む OSC 52 シーケンスをブラウザが解釈し、システムクリップボードへ反映します (xterm.js は OSC 52 を無視するため、ccserver 側で処理)。
- TUI ネイティブスクロール: opencode は独自の代替画面バッファでスクロールするため (xterm.js 自体のスクロールバックは効きません)、マウスホイール/タッチドラッグは合成ホイールイベントとして、ターミナル下部のスクロールボタンは opencode のメッセージスクロールキー (PageUp/PageDown, Ctrl+G/Ctrl+Alt+G) として中継されます。
- 列数の確保: 狭い画面 (スマホ等) では、opencode のプロンプト表示 (agent · model · provider 行) が折り返して画面の大半を占領しないよう、68 列を下限にフォントサイズを自動で縮小します。
- Usage ボタン (下記) は Claude Code の
/usage専用のため、opencode セッションでは非表示になります。
GitHub Copilot を選んだ場合:
- コマンドは
copilot。認証情報 (~/.config/github-copilotのhosts.json) と設定 (~/.copilot) はサンドボックスにも rw で見えるため、ログイン状態・モデル選択・セッション履歴はサンドボックス起動でも維持されます。 - 再開は
copilot --continue(最後のセッションへの再開) のみです。会話 ID を指定しての再開はできません (copilot の TUI は ID を出力しないため)。exit 後のセッション一覧からの再開や、予約プロンプト発火時の自動復帰も--continueで行われます。 - モデル入力欄に入れたモデル名は
--model <model>として渡されます。 - Usage ボタンは Claude Code 専用のため非表示になります。
- コンボ起動 (下記) では選択できません: copilot には MCP を CLI 引数/環境変数で注入する仕組みが無い (設定ファイル経由のため) ので、グループメンバーにしても ccserver の MCP broker ツールが使えません。コンボのメンバーには claude / opencode のみ選べます。
ターミナルヘッダの時計 (⏰) ボタンから、指定時刻に任意のプロンプトを自動投入できます。 5 時間の利用制限で停止したとき、解除時刻に「続けて」などを予約しておくと自動再開します。
- 時刻は サーバーのタイムゾーンで解釈されます (Claude Code が表示する制限解除時刻と一致)。パネルに現在のサーバー時刻とタイムゾーンを常時表示します。
- 過ぎている時刻は翌日として扱います。
- 予約はディスク (
.scheduled-prompts.json) に永続化され、ブラウザを閉じても、サーバーが再起動・クラッシュしても発火します。発火時にセッションが生きていなければ、claude --resume(opencode はopencode -c、copilot はcopilot --continue) で会話を自動復帰させてからプロンプトを投入します (元の cwd / サンドボックス設定も復元)。サーバー停止中に発火時刻を過ぎた予約は、起動直後にまとめて発火します (12 時間以上前に過ぎた物は破棄)。
エージェントが自分で呼べる通知ツール notify を提供する MCP サーバーです。旧来の「一定時間アイドル → ブラウザに input_needed 通知」というヒューリスティックは実質機能していなかった (アイドル判定が主観的・非フォーカス時のみ等) ため廃止され、この MCP に置き換わりました。
-
配信先は 2 種類で、両方に同時配信されます:
- Discord webhook —
sandbox.config.jsonのnotify.discordWebhook(https のみ) または環境変数CCSERVER_DISCORD_WEBHOOK(こちらが優先)。webhook URL は.gitignore済みのsandbox.config.jsonに入れるため、リポジトリに混入しません。 - ランタイム購読 (webhook URL) — MCP ツール
subscribeで登録した任意の webhook (unsubscribeで解除、list_subscriptionsで一覧)。購読は.saved-notifications.jsonに永続化され、サーバー再起動後も生き残ります。
- Discord webhook —
-
設定例 (
server/sandbox.config.jsonに追記):{ "notify": { "discordWebhook": "https://discord.com/api/webhooks/...", "subscriptions": [ { "url": "https://hooks.example.com/slack", "name": "slack" } ] } }subscriptionsは初期購読のシードです。購読ゼロ + Discord 未設定だと MCP 自体が注入されないため、購読だけから始めたい場合はここで seed します (MCP が無いとsubscribeを呼べないため)。 -
発信元属性 (自動付与): 各通知のペイロード末尾に、どのセッションから送られたかを示すフッターが自動で付与されます (
notify.hostname未設定なら OS の hostname、CCSERVER_HOSTNAME環境変数が最優先):🚨 Build failed details here _from: myhost · myproject · group abc12345 · session 01234567<host>は常に付与 (複数ホストで同じ webhook を共有する場合はnotify.hostnameで固定できます)。<project>はセッションの cwd の basename、group <…>はコンボのグループ ID 先頭 8 文字 (スタンドアロンでは付かない)、session <…>はセッション ID 先頭 8 文字です。- フッターを出したくない場合は
notify.attribution: falseで丸ごと無効化できます (既定true)。
-
注入条件: スタンドアロン (グループ外) のエージェントセッションと、コンボ起動のオーケストレーターのみに注入されます。シェル・コンボのワーカーには注入されません。サンドボックス内外どちらでも動作します (サンドボックス内はソケットを bind、外はホストの node でブリッジを実行)。
-
ツール:
ツール 引数 説明 notifytitle,body,level?(info/success/warning/error)全チャネルへ配送。 { ok, delivered: { discord, webhooks, failed } }subscribeurl(https のみ),name?webhook 購読を追加・永続化。 { ok, subscription }unsubscribesubscriptionId購読を削除・永続化。 { ok }/{ error: 'not-found' }list_subscriptions– { subscriptions: [...] } -
配送は Discord 互換 JSON
{ content, username: 'ccserver' }を globalfetchで POST します (10 秒 timeout)。失敗してもエージェント側にはエラーを返さず、ログのみ (非ブロッキング)。 -
予約プロンプト発火 (
schedule_fired) のブラウザ Notification とヘッダの通知トグルは独立した稼働機能のため温存しています。input_neededに関するブラウザ側のonAttention/ attention タブ表示も削除されました。
画面上部タブバー右端の Usage ボタンから、Claude Code の /usage (セッション / 週次の利用率・リセット時刻・プラン) をポップオーバーで確認できます。ボタンには現在セッションの使用率が常時表示されます (opencode / copilot セッションでは非表示)。
- 裏側では
claude --ax-screen-readerを短時間起動して/usageの描画をパースし、結果を約 2 分キャッシュします (/usageの閲覧自体は API を消費しません)。「更新」ボタンで即時に再取得できます。 - bwrap がある環境では、Claude の設定だけを見せる最小サンドボックス (docker/gpg/ssh なし) で起動します。無ければ claude を直接起動します。
- API:
GET /api/usage(?force=1で強制再取得)。サーバー起動時にキャッシュを 1 度ウォームします。 - ボタンは設定ファイルの
showUsage: falseで非表示にできます。さらに claude がサーバーにインストールされていない環境では、設定に関わらず自動的に非表示になります (この場合GET /api/usageはclaude is not installed on this serverを返します)。
copilot はコンボ起動 (グループ) では選択できません — copilot は MCP を CLI 引数/環境変数で注入する仕組みが無く (設定ファイル経由のため)、グループメンバーにしても ccserver の MCP broker ツール (
send_input/wait_for_handoff等) が使えないためです。起動モーダルのコンボ UI には選択肢が表示されず、POST /api/groupsにapp: "copilot"を渡しても 400 で拒否されます。
コンボ起動 (2 ワーカー + オーケストレーター) では、オーケストレーターが MCP ツール send_input でワーカーのターミナルにテキストを流し込みます。実際に「ワーカーのタブが TUI ではなく素のシェルの $ プロンプトに落ちていた」状態を見落として長文の指示を送り、トラブルになったことがあります (一度はシェルプロセスごと終了、一度は eval の構文エラー)。この種の事故を避けるため:
- ワーカータブ (opencode 等) は、直前に TUI が描画されていたように見えても、実際には素のシェルの
$プロンプトに戻っていることがあります。send_inputのsettled: trueや、直前のread_outputで TUI が見えていたことは、送った内容を実際に TUI が受け取った保証にはなりません。 - 素のシェルにテキストが渡ると、バッククォート
`はコマンド置換として評価され、複数行テキストはシェルを継続入力待ち (>プロンプト) の状態にしてしまうことがあります。 - そのため
send_inputで送る指示は短く 1 行にまとめ、バッククォートや改行などの特殊文字を避けてください。詳細な指示をターミナルに直接貼り付けるのではなく、./tmp/配下の plan ファイルなど既存ファイルへのパス参照に留めるのが安全です。 - 送信後は必ず
read_outputで、command not found/許可がありません/unexpected token/evalのようなシェルエラーが出ていないか確認してください。 - シェルエラーが出てしまった場合、それを収拾しようとして空入力や Ctrl+C 相当の入力を送ってはいけません。継続入力待ちのシェルに対しては EOF のように作用し、シェルプロセスごと終了させてしまうことがあります (実際に一度そうなりました)。
get_tab_statusでexited: trueを確認したら、そのタブは諦めてclose_tab→open_tabで作り直してください。 - 新規に開いたタブに何かを送る前には、
read_outputで実際にアプリの TUI が描画されていることを確認してから送ってください。
- ハンドオフは失われません:
wait_for_handoffはタイムアウト ({timedOut:true}) 時にそのままもう一度呼ぶだけで安全です。誰も待っていない間に届いたハンドオフはキューに残り、また待機中に接続が切れてもイベントを消費しないため、再接続後の次のwait_for_handoffが必ず受け取ります。サーバー再起動後も未受信ハンドオフは残っています。 read_outputのscreen/screenAlt/screenIdleMsを使う: ワーカーのスピナー等の動的描画はカーソル移動と行消去でその場を書き換えるため、生のバイト列 (raw/text) からは「今見えている画面」を復元できません。サーバーはセッションごとに軽量な仮想画面 (ANSI 解釈) を維持しており、screenが現在の可視画面、screenIdleMsが画面が最後に変化してからの経過 (スピナーが回っていれば小さい値、静止プロンプトなら大きい値) です。stuck/busy 判定はtextやidleForMs(バイトベース) よりこれらを優先してください。get_tab_statusのscreenIdleMsも同様です。
オーケストレーターのサンドボックスにワーカーのディレクトリはマウントされません (プロジェクトファイルへの直接アクセスは不可)。代わりに、control MCP サーバーのツール repo_info がグループのプロジェクト (cwd) の基本情報だけを返します: トップレベルの構成 (ディレクトリ/ファイル名のみ、100 エントリ上限)、README の先頭 ~8KB、package.json の要約 (name/version/description と scripts/dependencies/devDependencies のキー一覧のみ、値は返さない、各 50 キー上限)、git 状態 (現在ブランチ / short HEAD / 直近 5 コミットの件名 / 変更ファイル数)。
パス引数は受け取らず (対象はグループのプロジェクトに固定)、読み取り専用で、返る情報はすべてサイズ上限付きです。ソースファイルの内容は返しません。深い調査・コマンド実行・コード修正・判断を伴う作業は、従来どおり MCP ツール (send_input 等) 経由でワーカーに任せる必要があります。
「🔒 サンドボックスで起動」(上記「使い方 > 起動」参照) を選ぶと、bwrap でファイルシステムを制限した状態で起動します。選択したプロジェクトと最小限の設定 (~/.claude, ~/.claude.json, ~/.config/opencode, ~/.local/share/opencode, ~/.local/state/opencode, ~/.config/github-copilot, ~/.copilot 等) だけが見え、隣接する他プロジェクトは見えません。
docker も安全に使えるよう、サンドボックス内部に rootless dockerd を起動します。rootlesskit (subuid マッピング) の内側で bwrap を動かす構成のため、docker run -v ... でもサンドボックス外へは到達できません (daemon 自身が制限された FS の中にいるため)。
# Debian/Ubuntu
sudo apt install uidmap slirp4netns
# rootlesskit / docker (rootless) が入っていること。/etc/subuid, /etc/subgid にエントリが必要。uidmap/slirp4netns が無い場合は docker 無効のサンドボックス (bwrap のみ) として起動します。
サンドボックス内から git/gh/ssh/gpg を安全に使うための仕組みです。オン/オフは「使い方 > 起動」のモーダル (ディレクトリ単位) か、下記「設定ファイル」の gpg/sshAgent/gitBroker (サーバー全体の既定値) で制御します。
gitBroker (既定 on) が有効なとき、サンドボックス内の git アクセスはそのセッションの作業ディレクトリ自身のリモート + サブモジュール (再帰) から起動時に一度だけ算出した owner/repo にだけ制限されます。設定不要、~/.config/gh や ~/.ssh を binds に足す必要はありません (足してもブロックされ、警告が出るだけです)。
git の credential.helper がホスト側の git-broker プロセス (サンドボックスの外で動作、gh auth token を都度取得) に host+path を問い合わせ、許可されたリポジトリだけにトークンを渡します。トークン自体はサンドボックス内のファイルには一切現れません。
/usr/bin/ssh と $GIT_SSH_COMMAND を、起動時に読み取り専用で渡された許可リスト (gitBroker が算出したのと同じ owner/repo) と照合するラッパーに差し替えます。許可されなければネットワークに出る前に拒否されます。
ただし認証自体 (署名) は素通しなので、SSH の git remote を使うには別途 ssh-agent 転送を有効にしておく必要があります。これは HTTPS git (gitBroker で完結) にもコミット署名 (下記 gpg の領分) にも必須ではなく、必要なのは SSH の git remote を使う場合とサンドボックス内から素の ssh コマンドを直接叩きたい場合だけです。有効にすると、ccserver が起動時にユーザーの agent ソケット (/tmp/ssh-*/agent.* 等、鍵がロードされている物を優先) を探して SSH_AUTH_SOCK を設定します (env.SSH_AUTH_SOCK で上書き可)。
転送された agent はそのセッションの間、サンドボックス内のあらゆるプロセスから無制限に使える生の鍵アクセスになる点に注意してください (git 用途に絞られません) — 既定オフなのはこのためです。
有効にすると、~/.gnupg とホストの生 gpg-agent / keyboxd ソケットをサンドボックス内へ転送します。ホストの agent (鍵/トークンを保持) で署名するので、docker 有効のままコミット署名が使えます。ssh-agent 転送とは独立したフラグで、こちらだけ有効にしても ssh-agent は転送されません。
gitBroker が有効なとき、サンドボックス内の gh は素通しではなく、同じ git-broker プロセスへの中継に差し替わります。gh の API 呼び出しは TLS で api.github.com に直結するため通信内容を見て絞ることはできませんが、代わりに決め打ちの安全なサブコマンドだけをブローカーが実 gh (ホスト側、実際の認証情報付き) で代行実行し、対象リポジトリを git と同じ許可リストと照合します。トークンやCookieがサンドボックス内に渡ることはありません。
- 許可:
pr(create/view/list/edit/comment/merge/close/reopen/ready/review/checks/diff/status/checkout)、issue(create/view/list/edit/comment/close/reopen/status)、release(create/view/list/edit/delete/upload/download/delete-asset)、workflow(view/list はcwdフォールバック可、run/enable/disableは--repo/-Rの明示が必須 — 下記)、run(list/view/watch)、repo view、gh apiはrepos/OWNER/REPO/actions/...への読み取り専用 GET のみ。エンドポイントの owner/repo はリテラル記述が必須で、{owner}/{repo}プレースホルダ形式は受け付けません (gh がプレースホルダを独自の基準リポジトリ解決 (cwd origin /GH_REPO/--repo) から埋め、常に既定の API ホストへ投げるため、ブローカーが照合したリポジトリと実際の要求先が食い違う可能性がある)。 workflow run/enable/disableはトリガー/書き込み系 (CI 起動、workflow 自体の on/off) のため、--repo/-Rの明示が必須で、作業ディレクトリの origin への暗黙フォールバックは行いません。この制限はrun/enable/disableのみに適用され、以前は--repo無しでもそのまま通っていた挙動を狭める破壊的変更です (それ以外の読み取り系サブコマンドは従来通り cwd フォールバック可)。- 対象リポジトリは
--repo/-Rフラグ (OWNER/REPO,HOST/OWNER/REPO, URL) があればそれを、無ければ作業ディレクトリの origin リモートを使い、いずれも許可リストと照合されます (--repoで許可リスト外のリポジトリを指定しても拒否されます)。 pr view/checkout/diff/merge/close/edit等は<number>|<url>|<branch>を、repo viewは裸のOWNER/REPOも受け付けます。PR/issue の URL をそのまま位置引数に渡した場合、そのURLが指すリポジトリも許可リストと照合されます (--repo/cwd の判定をすり抜けて無関係なリポジトリを操作させることはできません)。- バンドルされた短縮フラグ (
-wR owner/repoのような1トークンへの複数フラグの結合) は拒否されます: gh (pflag/Cobra) はこの形を-w -R owner/repoと等価に解釈しますが、ブローカー側でこれを正しく再現するのは複雑で壊れやすいため、-R単体または-Rvalue(値を直接くっつける形) 以外の複数文字の短縮フラグはまとめて拒否します。個別のフラグ (-w単体等) はそのまま使えます。 - 拒否:
gh apiの Actions 以外のエンドポイント (graphql、/user、/orgs/...、repos/.../actions以外のrepos/...系、絶対URL、{owner}/{repo}プレースホルダ形式、POST 等の書き込み系 — Actions 配下でも--methodは GET のみ、データ系フラグ-f/--raw-field/--field/--input、--hostname、短縮フラグは全面的に拒否)、gh auth/gh secret/gh variable/gh ssh-key/gh gpg-key(認証情報自体の管理)、gh repo clone/fork/create/delete/rename(対象リポジトリが位置引数で来るため個別のパース対応が必要で未対応)、gh run rerun/cancel/delete/download(トリガー/書き込み系) など、上記に無いものは全て拒否されます。 - ブローカー越しの実行はホスト側で TTY なしの子プロセスとして動くため、非対話的な呼び出し (必要な入力は全てフラグ/stdin で渡す) のみサポートします。エディタが開く対話フロー (
gh pr createをフラグなしで叩く等) は動作しません。
gitBroker: false で git 側のゲート・gh ブローカーの両方を無効化できます (git は使えますが ssh-agent が有効なら無制限に、gh はそのまま実行されますが ~/.config/gh が無いため無認証で失敗します)。
これは「侵害/暴走したプロセスが無関係なリポジトリの認証情報を安易に使ってしまう」事故を防ぐ多層防御であり、意図的にバイパスを試みるコードへの完全な防壁ではありません。以下は主に ssh-agent 転送が有効なときに関係します (既定オフなら SSH 経由の抜け道はそもそも存在しません):
- 転送された ssh-agent ソケットに対し
sshバイナリを経由せず直接 ssh-agent プロトコルを話すコードは、宛先チェックをすり抜けて任意ホスト向けの署名を依頼できます。 docker: true(既定) と ssh-agent 転送を併用する場合、上記よりずっと簡単な迂回経路があります: サンドボックス内からdocker runされたコンテナはサンドボックス自身の/usr/bin/ssh・gh差し替えを引き継がず、独自のイメージ内の素のssh/ghを使えます。転送されたSSH_AUTH_SOCKは固定の既知パスにバインドされているため、コンテナ側に-vでそのソケットを渡すだけで、ラッパーを一切経由しない無制限の ssh-agent アクセスになります。つまり docker + ssh-agent 転送有効時は gitBroker のリポジトリ制限を強い境界として当てにしないでください。厳密なスコープが必要なセッションでは ssh-agent 転送を無効 (既定) のままにするかdocker: falseにするか、サンドボックス起動時にコンソールへ出る警告を確認してください。- サブモジュールの URL は、実際にチェックアウト済み (作業ディレクトリが存在する) のものだけを許可リストに加えます。
.gitmodulesはリポジトリのコンテンツそのものであり信頼できないため、宣言されているだけで未チェックアウトの「サブモジュール」は無視されます (信頼できないリポジトリがでっち上げの URL を許可リストへ紛れ込ませるのを防ぐため)。 - 許可リストはセッション起動時に一度だけ算出するため、セッション中に追加/チェックアウトしたサブモジュールや変更した gh の許可サブコマンドは次回起動まで反映されません。
サーバー全体の既定値です。各フラグは「使い方 > 起動」のモーダルでディレクトリ/ブラウザ単位に上書きできるものと (gpg/sshAgent/defaultApp)、この設定ファイルでしか変えられないものがあります。
cp server/sandbox.config.example.json server/sandbox.config.json
# 場所を変える場合: CCSERVER_SANDBOX_CONFIG=/path/to/config.json{
"docker": true,
"gpg": true,
"sshAgent": false,
"gitBroker": true,
"forceSandbox": false,
"defaultApp": "claude",
"showUsage": true,
"notify": {
"discordWebhook": "",
"subscriptions": []
},
"binds": [],
"env": {}
}| キー | 既定 | 説明 |
|---|---|---|
docker |
true |
サンドボックス内部で rootless dockerd を起動。false で無効 (軽量・rootlesskit 不要)。 |
gpg |
false |
コミット署名用に gpg-agent を転送 (上記参照)。UI で上書き可。 |
sshAgent |
false |
ssh-agent を転送 (上記参照)。UI で上書き可。 |
gitBroker |
true |
git/gh の認証情報スコープ制限 (上記参照)。 |
forceSandbox |
false |
true でサンドボックス外の起動を全面禁止。エージェント・シェルを問わず全セッションがサンドボックス強制になり、UI のサンドボックス切替は無効化されます。bwrap が無い環境 (または Windows) では起動をエラーで拒否します (/usage 取得の直接起動フォールバックも同様に禁止)。ホストに bwrap (bubblewrap) のインストールが必須です。 |
defaultApp |
"claude" |
新規セッションの既定エージェント ("claude"、"opencode"、"copilot")。UI で一度明示的に選んだ後はブラウザの記憶が優先され、この値は初回表示時の見た目とサーバー側フォールバック (予約プロンプトの自動再開など、クライアントが app を指定しない経路) にのみ使われます。コンボ起動のメンバーには適用されません (コンボのロール別選択は別途ブラウザの localStorage に記憶され、copilot はそもそも選択不可)。 |
showUsage |
true |
タブバー右端の Usage ボタン (Claude Code の /usage) を表示するか。false で非表示。claude がサーバーに無い場合は設定に関わらず自動的に非表示になります。 |
binds |
[] |
追加で見せるホストパス。各要素 { src, mode?, dest? }。mode は ro (既定) か rw。存在しないパスはスキップ。~ はホームに展開。~/.ssh と ~/.config/gh は gitBroker の設定に関わらず常にブロックされます。 |
env |
{} |
サンドボックス内の追加環境変数 (適用順は最後 = 既定値を上書き)。例: sshAgent: true のときに SSH_AUTH_SOCK を明示指定して自動検出を上書き。 |
claudeBin |
自動検出 | claude/opencode/copilot の起動方法。claude を PATH から解決し、ラッパー (例: /usr/bin/claude → /opt/claude-code/bin/claude) の場合は実体のインストール先を辿ってサンドボックスへ自動的に公開します。opencode は PATH に加えて ~/.opencode/bin も自動探索。copilot は PATH (SANDBOX_PATH) で自動解決されます (通常 ~/.local/bin/copilot)。自動検出で外れる場所にある場合や特定ビルドに固定したい場合のみ絶対パスで指定 (環境変数 CCSERVER_CLAUDE_BIN が優先。copilot に個別の bin 設定はありません)。 |
notify |
{} |
通知用 MCP (ccserver-notify) の設定 (上記「ccserver-notify (通知用 MCP)」参照)。discordWebhook は https のみ (非 https は無視)、subscriptions は初期購読 (https のみ)。CCSERVER_DISCORD_WEBHOOK 環境変数で discordWebhook を上書き可。 |
サンドボックスは Linux 限定です。同じプロジェクトを 2 つのサンドボックスで同時に開いた場合、docker の data-root 競合を避けるため 2 つ目は docker 無しで起動します。
ccserver → rootlesskit (subuid userns + slirp4netns) → bwrap (FS制限) → dockerd + claude/opencode
rootless docker には subuid マッピング付き userns が要るため、外側を rootlesskit、内側で bwrap が FS を制限します (この順序でないと newuidmap が使えずマルチ uid が壊れます)。/run は bwrap が専用 tmpfs で用意し (rootlesskit の --copy-up=/run は使わない)、ホストの生ソケットを bind ソースとして活かします。gpg は userns 内で uid 0 のため socketdir が ~/.gnupg になる点を利用し、生ソケットをそこへ転送しています。docker run -v ... でもサンドボックス外へは到達できません (daemon 自身が制限 FS 内)。
ccserver/
├── package.json # npm workspaces ルート + playwright
├── playwright.config.js
├── docs/
│ └── ccserver.service
├── tests/
│ ├── close-confirm.spec.js # Playwright E2E
│ ├── copilot-launch.spec.js # copilot 起動 + コンボ拒否 (copilot 未インストール環境では skip)
│ ├── mobile-scroll.spec.js # opencode TUI: タッチドラッグ→合成ホイールイベント (opencode 未インストール環境では skip)
│ └── scroll-buttons.spec.js # opencode TUI: スクロールボタン→メッセージスクロールキー (同上)
├── server/
│ ├── package.json
│ ├── index.js # Fastify エントリポイント (トークン認証・静的配信含む)
│ ├── usage.js # `claude --ax-screen-reader` を叩いて /usage をパース・キャッシュ
│ ├── sandbox.config.example.json
│ ├── routes/
│ │ ├── dirs.js # GET/POST /api/dirs, GET /api/dirs/home
│ │ ├── sessions.js # GET/DELETE /api/sessions...
│ │ ├── files.js # GET/POST /api/files (アップロード/ダウンロード)
│ │ ├── system.js # GET /api/system-stats (CPU/メモリ/温度/GPU/IPMI/ストレージ)
│ │ └── usage.js # GET /api/usage
│ └── ws/
│ ├── terminal.js # WebSocket + node-pty ブリッジ (/ws/terminal)
│ ├── sessionManager.js # セッション・予約プロンプトの状態管理/永続化
│ ├── appLaunch.js # アプリ非依存の起動ロジック (resume引数・permission検出等)
│ ├── notify.js # ccserver-notify: 購読レジストリ + Discord/webhook 配送 + MCP ソケット
│ ├── mcpConfig.js # MCP 設定の生成 (ccserver / ccserver-notify、sandbox/host 両モード)
│ ├── mcpServer.js # control / handoff / notify 各 MCP サーバー (SocketTransport 含む)
│ ├── mcpBroker.js # Unix-socket MCP ブローカー (control/handoff はグループ毎、notify はプロセス毎 1 つ)
│ ├── mcpTools.js # control/handoff ツールの実装 (deps 注入)
│ ├── screenModel.js # read_output 用の軽量仮想画面 (ANSI 解釈 + 変化検知)
│ ├── sandbox.js # bwrap + rootless docker サンドボックス構築
│ ├── sandbox-entrypoint.sh
│ ├── sandbox-gh-wrapper.cjs # サンドボックス内 gh をブローカー中継に差し替え
│ ├── sandbox-ssh-wrapper.cjs # サンドボックス内 ssh を許可リストでゲート
│ ├── sandbox-git-credential-helper.cjs
│ ├── sandbox-mcp-wrapper.cjs # MCP stdio ↔ Unix socket の中継 (argv 'notify' で通知ソケットへ)
│ ├── sandbox-gitconfig / sandbox-known-hosts / sandbox-ssh-config
│ ├── git-broker.js # サンドボックス外で動く、リポジトリスコープの認証情報ブローカー
│ └── ghAllowlist.js / gitAllowlist.js (+ 各 *.test.js, appLaunch.test.js, sandbox-resolve.test.js, notify.test.js)
└── client/
├── package.json
├── index.html
├── vite.config.js
└── src/
├── main.jsx / App.jsx
├── auth.js # トークン認証 (CCSERVER_TOKEN)
├── themes.js
├── osc52.js # OSC 52 クリップボード同期のパーサ (+ server/ws/osc52.test.js)
├── hooks/
│ └── useNotifications.js
├── components/
│ ├── DirectoryBrowser.jsx
│ ├── TerminalView.jsx # 遅延ロード (初期バンドル削減)
│ ├── UsageButton.jsx
│ └── SystemMonitor.jsx
└── styles/
└── app.css
CCSERVER_TOKEN 環境変数を設定すると、/api と /ws 配下の全リクエストに Jupyter 風のトークン認証がかかります (未設定なら無効)。?token=<TOKEN> クエリか Authorization: Bearer <TOKEN> ヘッダのどちらかで通ります。クライアントは 401 を受けると prompt() でトークンを聞き、localStorage (ccserver-token) に保存して以降のリクエストへ自動付与します (client/src/auth.js)。
CCSERVER_TOKEN=some-secret NODE_ENV=production node server/index.js| メソッド | パス | 説明 |
|---|---|---|
| GET | /api/dirs?path=<path>&showHidden=1 |
指定パスのサブディレクトリ/ファイル一覧 |
| GET | /api/dirs/home |
{ home, defaultApp } — サーバーのホームディレクトリと、設定ファイルの既定起動アプリ |
| POST | /api/dirs |
{ parent, name } でフォルダ作成 |
| GET | /api/sessions |
実行中セッションの一覧 |
| DELETE | /api/sessions/:id |
セッションを終了する (予約プロンプトも解除) |
| GET | /api/files?path=<path> |
ファイルをダウンロード |
| POST | /api/files |
multipart アップロード (destination フィールド + ファイルパート) |
| GET | /api/system-stats?ipmi=1 |
CPU/メモリ/温度/GPU (nvidia-smi)/IPMI (要 ENABLE_IPMI=1)・load average |
| GET | /api/usage?force=1 |
Claude Code /usage のキャッシュ済みスナップショット (force=1 で即時再取得) |
GET /api/dirs のレスポンス例:
{
"current": "/home/user",
"parent": "/home",
"dirs": [
{ "name": "projects", "path": "/home/user/projects" }
],
"files": [
{ "name": "notes.txt", "path": "/home/user/notes.txt", "size": 123, "mtime": 1730000000000 }
]
}JSON メッセージでターミナル I/O とセッション管理 (アタッチ・予約プロンプト・自動承認) を中継。
| 方向 | type | フィールド | 説明 |
|---|---|---|---|
| → | init |
cwd, cols, rows, claudeSessionId?, shell?, sandbox?, sandboxOpts?, app?, resume? |
新規セッションを起動 (app: "claude" (既定)、"opencode"、"copilot"、shell: true で素のシェル、resume: true で opencode/copilot の最終セッションに再開) |
| → | attach |
sessionId, cols?, rows? |
既存セッションに再接続 (出力バッファを replay で再送) |
| → | input |
data |
キーボード入力 |
| → | resize |
cols, rows |
ターミナルリサイズ |
| → | ping |
– | 疎通確認 (pong が返る) |
| → | set_auto_yes / get_auto_yes |
enabled? |
確認プロンプトの自動承認 ON/OFF・状態取得 |
| → | schedule_prompt |
time ("HH:MM") か at (epoch ms), text |
予約プロンプトを設定 |
| → | cancel_schedule / get_schedule |
– | 予約の解除・現在状態の取得 |
| ← | session |
sessionId, cwd, cols, rows, isReconnect |
スポーン/再接続完了 |
| ← | output |
data |
ターミナル出力 |
| ← | replay |
data |
attach 時、切断中に貯まった出力バッファを再送 (複数回届く) |
| ← | exit |
exitCode, signal, claudeSessionId |
プロセス終了 |
| ← | auto_yes_state |
enabled, log |
自動承認の状態変化・ログ |
| ← | schedule_state |
scheduled, serverTz, serverNow, error? |
予約プロンプトの現在状態 (サーバー時刻/TZ 付き) |
| ← | error |
message, code |
エラー通知 (SESSION_NOT_FOUND は自動再接続、それ以外はターミナルにメッセージを表示) |
| ← | pong |
– | ping への応答 |
cd /path/to/ccserver
npm run build --workspace=clientcp docs/ccserver.service ~/.config/systemd/user/ccserver.serviceまたは手動で ~/.config/systemd/user/ccserver.service を作成:
[Unit]
Description=ccserver — AI CLI web server
After=network.target
[Service]
Type=simple
WorkingDirectory=/path/to/ccserver
Environment=NODE_ENV=production
Environment=PORT=3001
ExecStart=/usr/bin/node server/index.js
Restart=on-failure
RestartSec=5
[Install]
WantedBy=default.target# ユーザーサービスのデーモンをリロード
systemctl --user daemon-reload
# 起動
systemctl --user start ccserver
# 自動起動を有効化
systemctl --user enable ccserver
# ログイン中でなくてもサービスを維持(必要に応じて)
sudo loginctl enable-linger "$USER"# ステータス確認
systemctl --user status ccserver
# ログ表示
journalctl --user -u ccserver -f
# 再起動
systemctl --user restart ccserver
# 停止
systemctl --user stop ccserverTailscale Serve を使うと、Tailnet 内のデバイスから HTTPS でアクセスできます。
systemctl --user status ccserver# ポート 3001 を HTTPS で公開
sudo tailscale serve --bg 3001これにより https://<hostname>.<tailnet>.ts.net/ でアクセス可能になります。
# 現在の serve 設定を表示
tailscale serve statustailscale serve --https=443 offMIT