経緯
6月某日より、Claude Pro Planに加入すると従量課金ではなく月額固定でClaude Codeを使えるようになった。そこで自分もようやくPro Planに加入し、改めてClaude Codeを試してみた。課金額を気にせずに使い続けられるのは、やはり心理的に大きな違いがあると感じる。
Claude CodeのTUIは十分に操作性に優れていて使いやすいと感じたが、自分としてはまだ不便に感じる点があった。
ターミナルでの日本語入力問題
英語がとにかく苦手なので、基本的に指示は日本語で書いている(これは完全に自分が悪いのだけど)。
自分はターミナル上で自然言語、特に日本語をタイプする習慣がない。AquaSKKが(普段使っているWezTerm含め)ターミナルアプリケーションとあまり相性が良くないというのが主な理由だ。VSCodeのTerminalでは特にひどくて使いものにならない(おそらくxterm.jsの問題っぽい)。
そんなわけで、チャットアプリケーションのような感覚でブラウザからClaude Codeに指示を出せるWebUIがあったら、入力もしやすくなって良いのではないかと思い、作ってみることにした。
技術仕様
- backend: Deno
- frontend: Vite + TypeScript + React
双方TypeScriptなので共通の型が使える。
backend
claude
CLIによるstreaming出力
claude
コマンドは、以下のようなコマンドでTUIではなくCLIの単発コマンドとして利用できる。
$ claude --verbose --output-format stream-json -p "hello" {"type":"system","subtype":"init","cwd":"...","session_id":"a458d2ce-2fa5-4fbe-b550-336a32858c37","tools":["Task","Bash","Glob","Grep","LS","exit_plan_mode","Read","Edit","MultiEdit","Write","NotebookRead","NotebookEdit","WebFetch","TodoRead","TodoWrite","WebSearch"],"mcp_servers":[],"model":"claude-sonnet-4-20250514","permissionMode":"default","apiKeySource":"none"} {"type":"assistant","message":{"id":"msg_01RxycWfS2d1CMZqs9QbFT3u","type":"message","role":"assistant","model":"claude-sonnet-4-20250514","content":[{"type":"text","text":"Hello! I'm Claude Code, ready to help you with your blog repository. How can I assist you today?"}],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":3,"cache_creation_input_tokens":13750,"cache_read_input_tokens":0,"output_tokens":1,"service_tier":"standard"}},"parent_tool_use_id":null,"session_id":"a458d2ce-2fa5-4fbe-b550-336a32858c37"}
このように文字列入力に対する応答をJSON streamingで受け取れるので、基本的にはこの出力をキャプチャしてfrontendに流すという仕組みになる。
Claude Code SDK
結果的に Claude Code SDK を使用することにした。基本的にはCLIコマンド実行をラップしているだけだが、TypeScriptの型定義が提供されており、queryやmessage、errorなどを記述しやすいというメリットがある。
session_idによる会話の継続
frontendからの1リクエストごとにCLIとして応答を返してプロセスが終了するため、セッションの継続性を確保する必要があった。
各会話の応答に session_id
というものが付与されており、--resume <sessionId>
オプションで繋いでいくことができる。バージョン 1.0.18
から 1.0.21
あたりではこのオプションで復元できない問題があったが、1.0.22
あたりからは修正され、チャット内で履歴を維持しながら連続した作業が可能になっている。
permission制御
Claude Codeをnormal modeで動かしていると、一部のToolの使用時にユーザの許可を求める仕組みがある。
TUIでは即座にインタラクティブなダイアログが表示され、ユーザが許可/不許可をリアルタイムで選択できるが、CLIのstream-json
出力では、permission requestは以下のようなエラーメッセージとして出力される:
{ "type":"tool_result", "content":"Claude requested permissions to use Bash, but you haven't granted it yet.", "is_error":true }
その後、I need permission to run bash commands. Please grant access to the Bash tool
のようなメッセージを出力してプロセスが終了する。
Claude Codeの内部動作は不明だが、ある程度推測し、以下のような動作をするよう実装した。
- Permission検知:
tool_result
でis_error:true
のメッセージを検知 - Session中断: 該当セッションを一時停止し、frontendにダイアログを表示
- 許可処理: ユーザが許可した場合、該当Toolを
allowedTools
リストに追加 - 続行: 「続きを実行」というメッセージでセッションを再開
allowedTools
はproject settingでも永続的に指定できるが、セッション単位で揮発するものもある。今回はWebアプリのメモリ上でセッション単位の許可を管理している。
実際、TUIでも同様の仕組みで動作しているのではないだろうか。
Denoについて
特にDenoを使うことに強いこだわりはなかったが、バイナリを配布できるのが利点だと思った。ただしファイルサイズは少し大きくなってしまうが…。
https://github.com/sugyan/claude-code-webui/releases
frontend
backendからは ReadableStream
でレスポンスを返すので、それを逐次読み取って表示処理を行う。最初はWebSocketなどの方が良いか検討したが、そこまでする必要はなさそうということでシンプルにした。
TailwindはAIに書かせるときれいにやってくれて良い。ただし学習が足りていないのか、v4でDark mode対応をさせようとしたら全然うまくいかず大変だった。
Claude Codeによる実装
最初のアーキテクチャ設計や動作仕様の策定は自分で行ったが、実装コードはすべてClaude Codeに任せてみた。自分が手動で書いたコードは1行もcommitしていない。
GitHub上でOSSとしての開発進行
ワークフローの整備
最初はローカルで次々作っていくだけだったが、途中からはプルリクエストを提出してマージしていく開発スタイルで進めた。Claude Codeがコードを書いてプルリクエストを作り、自分(とCopilot)がレビューしマージを担当する形で、一般的なチーム開発のようなフローにした。
プルリクエストに対するGitHub Actionsによる自動テストや品質チェックは非常に有用だった(もちろんそれらのActionsのYAMLもClaudeに書かせている)。CLAUDE.md
ではlintやformat、testなどを実行してからコミットするよう指示しているが、Claude Codeは結構頻繁に抜け漏れがある。
しかしCIでそのあたりは確実にチェックしてくれるので、漏れを防ぐことができる。「CIが失敗しているので修正してください」と伝えるだけで修正してくれるし、GitHub Copilotのコードレビューにも「対応すべきものは対応してください」と簡単に指示するだけで対応してもらえる。
記録としてのissue駆動開発
Plan modeで直接仕様を伝えて実装させることも可能だが、あえてIssueを経由するワークフローを採用した。
- Issue作成: まずClaude Codeに機能要件を整理してIssueとして記録してもらう
- 実装指示: 「Issue #35を解決する実装を進行し、プルリクエストを提出してください」と指示
作業ボリュームが大きい場合は、サブIssueとして分割することも検討してもらう。
特に大きなメリットはないかもしれないが、指示を出す側としても作業ボリュームについて感覚がつかめるし、Claude Codeがどう問題を分析し、どんな解決方針を立てたかが記録として残るため、「思考プロセスの可視化」という点では価値があると思う。自分がふわっと出した指示もIssueとして明文化されることで、問題や実装方針が整理される。
templateは人間向けのツール
pull_request_template.md
を用意してみたが、Claude Codeはなかなかそれに従ってくれないことが分かった。gh pr create
コマンドでは--body-file
でtemplateを指定してから編集するくらいしか方法がないので、仕方ないのかなとは思う。
Claude Codeは何もテンプレートなど与えなくても必要な情報を記載した丁寧なPRディスクリプションを作成してくれる。
つまりテンプレートは「人間にきちんと書かせるためのツール」であり、AIにはそもそも必要ないものだった。
開発速度のインパクト
品質には気を遣っているため、実装後のコードレビューや動作確認は必ず行っている。しかしそれでも、一人で開発するよりもはるかに高速で機能実装が進められている。
片手間で作業させていても数日で次々と機能実装を進めてリリースできている。
https://github.com/sugyan/claude-code-webui/blob/main/CHANGELOG.md
自分一人で書いていたら、作れないことはないとは思うが、こうはいかなかっただろう。
今後の展望
現時点ではTUI版と比較してまだまだ足りない機能があるが、見つけ次第issueにしてClaude Codeに実装させていく予定。特に以下のような点が課題として残っている:
- モードの切り替え
- slashコマンドなどの対応
- 作業履歴の検索、そこからの復元
あとはブラウザならではの機能として、作業終了時にpush通知する、あるいはText-to-Speech APIで喋らせる、といった機能をつけていったら面白いかもしれない。
最終的には、ターミナルからClaude Codeを立ち上げる必要がなくなり、完全にブラウザベースで開発作業ができるところまでいけるだろうか。
まとめ
Claude CodeのWebUI化というアイデアから始まったこのプロジェクトは、結果的に多くの学びをもたらしてくれた。
技術的には、Claude CodeのCLI出力をストリーミングで処理し、permission制御を実装することで、TUIに近い体験をWebブラウザで再現できることが分かった。Claude Code SDKの活用により、比較的シンプルな実装で実現できたのも収穫だった。
開発プロセス面では、AI主導の開発でも適切なワークフロー(CI/CD、コードレビュー、Issue管理)を組み合わせることで、品質を保ちながら高速で開発を進められることを実感できた。Claude Codeの「思考プロセスの可視化」としてのIssue駆動開発も、予想以上に有効だった。
何より、日本語入力の問題という個人的な課題から始まったプロジェクトが、Claude Codeの可能性を探る良い実験台になったことが一番の成果だと思う。Claude Codeの使い方の練習という意味でも、非常に良いプロジェクトになった。