痛み: 「/home/yousan/.claude/usage-data/report-...html です」と言われても開けない
OpenClaw で AI エージェントを動かしていると、エージェントは仕事の成果物をホスト上にファイルとして生成する。HTML のレポート、PDF、動画、スクショ、CSV。生成した結果としてエージェントが返してくるのは 「ホスト上の絶対パス」 だ。
できました: /home/yousan/.claude/usage-data/report-2026-06-12-111053.html
これを Discord や LINE 越しに受け取った私は、開けない。
- ファイルパスは URL ではない。Discord でクリックしても何も起きない
-
同じマシンに居ても、Finder で
~/Library/...を掘るなりopen <path>を打つなりが要る - 別マシンに居ると SCP するか SSH してブラウザを開くしかない
- iPhone から見ようとした瞬間に詰む。SCP も SSH も実用的じゃない
私の環境では OpenClaw が動いてるのは Windows 上の WSL Ubuntu (tachikoma)、私が触ってるのは Mac、外では iPhone、というクロスホスト・クロスネットワーク構成なので、 エージェントが何かファイルを作るたびに「見るための儀式」が発生 していた。これがしんどい。
エージェントの成果物の大半は HTML/画像/PDF/動画なので、本来は HTTP(S) で URL になっていれば、デバイス問わずブラウザで一発 のはずだ。これをどうにかしたい、というのが今回の話。
結論: tailnet 限定の Caddy 配信 + 日付別 share/ + エージェントへの規約
採用した形はこれ。
-
ホスト: OpenClaw が動いているマシン(私の場合は
tachikoma) -
bind: Tailscale のノード IP に固定(
100.x.y.z)。0.0.0.0にはしない -
配信: Caddy の
file_server browseを systemd の user unit として常駐 -
ルート:
~/.openclaw/workspace/share/ -
構造:
share/YYYY-MM-DD/<topic-slug>/ - 置き方: 元ファイルは触らず symlink を置くだけ
- アクセス: Tailscale MagicDNS でホスト名解決。Mac でも iPhone でも tailnet に入っていればそのままブラウザで開ける
これで「ファイルパス問題」は次のように形が変わる。
- エージェントは生のホストパスを返す代わりに、
share/YYYY-MM-DD/<topic-slug>/に symlink を作って URL を返す - 私は Discord で URL を受け取り、Mac でも iPhone でもタップ一発で開く
- SCP も SSH も Finder も登場しない
そして地味に効くのが Tailscale で、これが2つの問題を同時に潰してくれる。
- ネットワーク問題: WSL Ubuntu と Mac と iPhone は通常別ネットワークだが、tailnet 内なら直接到達できる
-
ホスト名問題: MagicDNS のおかげで IP を覚えなくていい(
http://tachikoma:8801/)
さらに share/YYYY-MM-DD/ で日付別に切ってあるので、Caddy の autoindex を辿るだけで「今日エージェントが何を作ったか」を一覧で見渡せる。
当日に潜るとトピック単位で並ぶ。
Caddy の file_server browse には Grid 表示と画像サムネが付いてくる。生成画像を渡すときはこっちのほうが圧倒的に見やすい。
実際の成果物を開くとこう(ヘッドレス Chrome で撮った)。Claude Code の利用状況レポートをエージェントに生成させて、ホスティングだけお願いした例。
なぜ Caddy なのか — 候補3つを試した結果
最初は python3 -m http.server で立てていた。これは確かに動くんだが、autoindex の見た目が <ul><li> の素の HTML で、ファイルが増えてくると一覧性が落ちる。スマホから見ると文字も小さくてつらい。
そこでファイラ系UIを持つ候補をいくつか試した。
| 候補 | symlink 追従 | UI | systemd 化 | 認証 |
|---|---|---|---|---|
python3 -m http.server |
する | 素のテキスト一覧 | 容易 | 無し |
| filebrowser | しない1 | リッチなファイラ | 容易 | あり |
| dufs | する | テーブル型 | 容易 | あり |
Caddy file_server browse |
する | List/Grid/サムネ/検索/ソート | 容易 | リバプロで足せる |
結局 Caddy に落とした。理由は単純で、
- symlink を素直に追従する。share の外を指す symlink でも何の設定もなく見える(これは大きい)
- Grid 表示と画像サムネ がデフォルトで効く。生成画像が多い私のユースケースに合う
-
ダークモード が OS の
prefers-color-schemeに追従する。夜の iPhone でまぶしくない - 検索・ソート・パンくず が全部入り。File ロード時に JS が走る系ではなく、サーバ側 HTML なので軽い
- シングルバイナリ。Go 製で 50MB 弱、依存ゼロ
- 設定は Caddyfile 8行。実質ワンライナーみたいなもの
filebrowser が外れたのは認証 UI のせいではなく純粋に symlink の挙動。 ユーザが share の中だけにファイルを置く運用に変えれば filebrowser でもいけるが、本記事の「元ファイルは触らない」原則と相性が悪い。dufs は問題なく動くがテーブル型 UI で Grid・サムネが無い、というだけ。
構築
1. ディレクトリを切る
mkdir -p ~/.openclaw/workspace/share
2. Caddy を入れる
公式のシングルバイナリを取ってくるのが一番速い。
mkdir -p ~/bin
curl -sL "https://caddyserver.com/api/download?os=linux&arch=amd64" -o ~/bin/caddy
chmod +x ~/bin/caddy
~/bin/caddy version
apt のパッケージ(caddy)でも構わないが、share 配信用途では root 権限を要求してまで /etc/caddy/Caddyfile を使う必要はない。
3. Caddyfile を書く
~/.config/caddy/Caddyfile:
{
admin off
auto_https off
}
:8801 {
bind 100.100.26.90
root * /home/yousan/.openclaw/workspace/share
file_server browse
encode gzip
}
ポイントは2つ。
-
bindで listen インターフェースを tailnet IP に絞る。Caddy のリスナーは、サイトアドレスに IP を書いてもデフォルトで全インターフェース (0.0.0.0) に bind される。tailnet 限定にするにはbind 100.100.26.90を明示するのが必須で2、これを忘れると家 LAN 全体にポートが開く。そしてサイトアドレスは ホストを書かず:8801にする — ここにホスト名や IP を書くと Host マッチが効いて、http://tachikoma:8801/(MagicDNS 名)でのアクセスが弾かれるからだ3 -
auto_https off。tailnet 内通信なので TLS 要らず。これを書かないと Caddy が証明書を取りにいこうとする
4. systemd user unit を書く
~/.config/systemd/user/share-http.service:
[Unit]
Description=Workspace share static HTTP server (Caddy, tailnet only)
After=network-online.target tailscaled.service
Wants=network-online.target
[Service]
Type=simple
ExecStart=%h/bin/caddy run --config %h/.config/caddy/Caddyfile --adapter caddyfile
Restart=on-failure
RestartSec=3s
[Install]
WantedBy=default.target
tailnet IP は Caddyfile 側で持つので unit はシンプル。
5. 起動して enable
systemctl --user daemon-reload
systemctl --user enable --now share-http.service
systemctl --user status share-http
systemctl --user を使うので、サーバマシンでは loginctl enable-linger $USER4 を一度叩いておくとログアウト後も生き続ける。
6. エージェントへの規約として書き起こす(ここが本題)
サーバが立っているだけでは何も解決しない。エージェントが 「成果物のパスを返す代わりに symlink を置いて URL を返す」 ようになって初めて、痛みが消える。
OpenClaw には memory/ という長期メモリ機構があって、エージェントは起動時にこれを参照する5。ここに「ファイルを渡すときのルール」を書いておけば、すべてのエージェントが同じ規約に従って成果物を出してくれるようになる。
私が memory/tailscale-static-hosting.md に書いているのはこれ。
---
name: tailscale-static-hosting
type: feedback
---
yousan にファイル/動画/レポート等をブラウザで渡すときは、毎回新ポートを立てずに統合 share 領域に置く。
**Why:** 過去にエージェントが各自でポート立てて入口が散乱した。
yousan が「どこ見ればいい?」を毎回確認しないと辿れなかった。
**How to apply:**
- ホスト: tachikoma, tailscale IP 100.100.26.90
- 配信プロセス: share-http.service (systemd user unit, Caddy)
- ルート: /home/yousan/.openclaw/workspace/share/
- 構造: share/YYYY-MM-DD/<topic-slug>/
- 実体: 元ファイルは触らず symlink を置く
- URL: http://tachikoma:8801/YYYY-MM-DD/<topic-slug>/
- 新規依頼が来たら: share/YYYY-MM-DD/<slug>/ を作る → symlink → URLを渡す。
サービス常駐済みなので再起動不要
- 禁止: 別ポートで HTTP サーバを新規に立てる /
Caddyfile の bind から tailnet IP を消す(全インターフェース公開になる)
ポイントは 「Why」と「How to apply」を両方書く こと。Why だけだと「で、どうすればいいんだ」になるし、How だけだと「これ何のためのルール?」になって、エージェントがエッジケースで判断を誤る。
これさえ書いておけば、「このファイルをホスティングしてほしい」と言うだけで、エージェントは自動的に symlink を置いて URL を返してくる。/home/yousan/.claude/usage-data/... というパスは二度と Discord に流れてこない。
エージェントが実際にやってること
参考までに、依頼を受けたエージェントが内部的に叩いてるコマンドはこんな感じ。
mkdir -p ~/.openclaw/workspace/share/2026-06-12/usage-report
ln -sf /home/yousan/.claude/usage-data/report-2026-06-12-111053.html \
~/.openclaw/workspace/share/2026-06-12/usage-report/index.html
index.html という名前にすると、ディレクトリ URL を叩いたときに Caddy の file_server がそれを返す6。autoindex のディレクトリリストが欲しいときは index.html を置かなければいい(browse を有効にしているのでリストが生成される)。
返ってくる URL はこう。
http://tachikoma:8801/2026-06-12/usage-report/
MagicDNS のおかげで tachikoma というショートホスト名で到達できる7。Mac でも iPhone でも、tailnet に入っていればそのままタップで開ける。
MagicDNS が無いノードからは FQDN (tachikoma.tail087bcf.ts.net) で行ける。
クロスデバイスで効くポイント
これを入れたあとで一番ありがたかったのは iPhone から開けるようになったこと だった。
| デバイス | 以前 | 今 |
|---|---|---|
| 同じホストの Linux |
xdg-open <path> で開く |
URL タップ |
| Mac (別ホスト) | SCP / sshfs / SSH 経由でブラウザ | URL タップ |
| iPhone | 事実上見られない | URL タップ |
「事実上見られない」が「URL タップ」になるだけで、外出中にエージェントから来た成果物を確認できるようになった。これが副次的だけど一番大きい。
加えて、
- 過去成果物が消えない: ポートが死なないので、3日前の URL を共有しても今でも開ける
-
露出範囲がブレない: 全エージェントが tailnet IP bind を使う規約なので、
0.0.0.0事故が起きる隙間がない - 元ファイルが汚れない: symlink なので、エージェントが「ホスティング用にコピー作ります」をやらない
- Grid 表示が画像レビューに強い: 生成画像をブラウザで一気にレビューできる
- autoindex が地味に効く: 何があったか日付単位で振り返れる
まとめ — 「こう書いておけば楽」
ここまでの話を OpenClaw に与えるルールに圧縮するとこうなる。
- OpenClaw が動くホストに Caddy を tailnet IP に bind して systemd user unit として常駐させる
- クライアント (Mac, iPhone) を Tailscale で同じ tailnet に入れる
-
memory/tailscale-static-hosting.mdのような形で エージェントに「成果物は share/YYYY-MM-DD// に symlink、URL を返す」と書く - 元ファイルは触らない、ポートは増やさない、tailnet IP bind は外さない、と明記する
これだけで「エージェントが返すホストパスをどう開くか問題」がほぼ消える。ファイル単位の SCP も SSH も Finder も要らなくなり、Mac でも iPhone でも URL タップだけで成果物にアクセスできるようになる。
参考リンク
- Caddy
- Caddy: file_server ディレクティブ
- Caddy: bind ディレクティブ
- Caddy: encode ディレクティブ
- Caddy: Caddyfile の構文
- systemd: ユニットファイル仕様
- systemd: loginctl と linger
- Tailscale
- Tailscale MagicDNS
- Tailscale ACL
- OpenClaw
- filebrowser
- dufs
-
filebrowser は
--rootの外を指す symlink を一覧に出さない仕様(2026-06 時点)。本記事の運用は「エージェントが各自の保存先に書く → share/ から symlink で参照」が前提で、symlink 先は基本 share の外なので filebrowser だと一覧が空に見える事故が起きる。 ↩ -
Caddy のリスナーはデフォルトでワイルドカード(全インターフェース)に bind する。サイトアドレスにホストや IP を書いてもそれは受け付ける
Hostヘッダのマッチに使われるだけで、listen するインターフェースは変わらない。特定 IP のインターフェースだけで listen させるにはbindディレクティブ が必須。 ↩ -
Caddy はサイトアドレスのホスト部分を
Hostマッチャとして扱う。100.100.26.90:8801と書くとHost: 100.100.26.90:8801のリクエストしか通らず、http://tachikoma:8801/のようなホスト名アクセスは弾かれる。ホスト部分を省いて:8801とすれば任意のHostにマッチするので、IP でもホスト名でも到達できる。 ↩ -
systemd の linger を有効にすると、ユーザがログインしていなくても user manager が起動し続ける。常駐 user unit に必須の設定。 ↩
-
OpenClaw のエージェントワークスペース(
~/.openclaw/workspace/)にはAGENTS.mdMEMORY.mdmemory/といったファイルがあり、セッション開始時に読み込まれる仕組みになっている。詳細は OpenClaw のドキュメント を参照。 ↩ -
Caddy の
file_serverは要求パスがディレクトリの場合、既定でindex.html/index.txtを順に探して見つかればそれを返す。なければbrowse設定が有効なときに限ってディレクトリリストを HTML で生成する。 ↩ -
Tailscale MagicDNS は tailnet 内のホスト名を自動で名前解決する仕組み。各ノードの
hostnameで他ノードからアクセスできる。tailnet に参加していない端末からは名前も解決できないし TCP も通らない。 ↩



