はじめに
しばらく前から、Linux 環境では Obsidian を Flatpak で動かしている。
Flatpak を選んだ理由は単純だ。パッケージ管理が楽で、サンドボックスによってシステムが汚れない。Obsidian のような外部配布バイナリを野良インストールするより、Flatpak のほうが更新管理が整っている。それだけのことだった。
しかし、コミュニティプラグインを本格的に使い始めると、話が変わってくる。
Git 同期、AI コーディング補助、ローカルCLI連携——こうした機能を担うプラグインは、ほぼ例外なく「シェルと同じ環境でサブプロセスを起動できること」を前提にしている。Flatpak のサンドボックスは、その前提を静かに崩す。
これは、その格闘の記録だ。
Flatpakのサンドボックスは「透過的に見えて、透過的でない」
まずFlatpakの挙動を整理しておく。
Flatpakは、アプリケーションを独立したサンドボックス内で動かす仕組みだ。ホームディレクトリはマウントされる。ネットワークも原則として通る。だから「普通のアプリ」として使っている限り、サンドボックスを意識する機会はほとんどない。
問題が起きるのは、アプリがサブプロセスを起動しようとするときだ。
Flatpak サンドボックスが制限するもの:
- プロセス名前空間(ホストのプロセスと分離)
- ファイルシステム(明示的に許可した場所のみアクセス可能)
- 環境変数(ホストのシェル設定は届かない)
Flatpak が通すもの:
- ネットワーク(デフォルトで許可)
- ホームディレクトリ(マウントされる)
「ファイルは見えるのに実行できない」「コマンドは存在するのに PATH に入っていない」——Flatpak 固有の症状は、このギャップから生まれる。エラーメッセージが表面に出ないことも多く、デバッグが難しい。
第一の壁:Obsidian Git と SSH agent
最初にぶつかったのは、Obsidian Git の SSH 認証だ。
ターミナルでは問題なく GitHub に接続できる。しかし Obsidian Git plugin を使うと毎回パスフレーズを求められる、あるいは認証が通らない。
原因は、SSH_AUTH_SOCK の到達範囲だった。
ターミナルで起動した ssh-agent がエクスポートする SSH_AUTH_SOCK は、そのシェルプロセスの子孫にしか届かない。GNOME アプリケーションランチャーから起動した Flatpak Obsidian は、そのプロセスツリーの外側にいる。
ターミナルの ssh-agent
└─ terminal プロセス
└─ zsh(SSH_AUTH_SOCK 有効)
← ここに壁がある
Flatpak Obsidian(SSH_AUTH_SOCK 届かない)
GNOME Keyring(gcr)で統一しようと試みたが、gcr-ssh-agent が署名処理で不安定になるという別の問題にぶつかり断念した。
最終的な解決策は、OpenSSH ssh-agent を systemd ユーザーサービスとして固定することだった。
# ~/.config/systemd/user/ssh-agent.service
[Unit]
Description=OpenSSH key agent
[Service]
Type=simple
Environment=SSH_AUTH_SOCK=%t/ssh-agent.socket
ExecStart=/usr/bin/ssh-agent -D -a %t/ssh-agent.socket
[Install]
WantedBy=default.target
固定された socket(/run/user/1000/ssh-agent.socket)を Flatpak に公開し、Obsidian Git 専用の wrapper script で SSH_AUTH_SOCK と BatchMode=yes を明示する。
詳細は別記事に書いた: → zsh に移行したら Obsidian Git が壊れた話 — SSH agent と Flatpak の格闘記
第二の壁:Codex CLI と stdio JSON-RPC
SSH agent を解決して、しばらく安定していた。次に問題が出たのは AI プラグインとの連携だった。
Obsidian で AI コーディング補助を使うとき、プラグインによって実装が大きく異なる。
| プラグイン + 連携 | 通信方式 | CLI の実体 | Flatpak 影響 |
|---|---|---|---|
| Smart Composer + Codex | HTTP API(REST) | — | ネットワークは通る → 問題なし |
| Claudian + Claude Code | stdio JSON-RPC | Bun standalone ELF(自己完結バイナリ) | 最初から動いた |
| Claudian + Codex CLI | stdio JSON-RPC | Node.js スクリプト(#!/usr/bin/env node) | 詰まった |
Smart Composer が Codex subscription でそのまま動いたのは、OpenAI の REST API を直接叩いているからだ。Flatpak はネットワークを遮断しない。
Claudian の Codex 連携がすぐには動かなかったのは、ローカルの codex CLI をサブプロセスとして起動し、stdio で JSON-RPC 通信をするからだ。
Claudian plugin
→ spawn: codex app-server --listen stdio://
→ JSON-RPC over stdin/stdout
ラッパースクリプトを書いた
Flatpak 内から /home/rbcn2000/.npm-global/bin/codex を呼ぶには、flatpak-spawn --host を使ってサンドボックス外のプロセスとして起動する必要がある。最初に書いたラッパーはこうだった。
#!/usr/bin/env zsh
exec flatpak-spawn --host zsh -i -c '/home/rbcn2000/.npm-global/bin/codex "$@"' -- "$@"
-i(interactive)フラグをつけた理由は、.zshrc を読み込んで nvm 経由の Node.js を PATH に乗せるためだ。しかしこれが問題だった。
zsh -i が stdout を汚染する
このスクリプトを Claudian の Codex CLI パスに設定しても、連携は動かなかった。エラーメッセージもない。
原因はこうだ。
zsh -i は interactive shell として起動するため、.zshrc をすべて実行する。このマシンの .zshrc には以下が含まれている。
eval "$(zoxide init zsh)" # → stdout に出力する可能性
eval "$(starship init zsh)" # → stdout に初期化メッセージを出す
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
Claudian の Codex 連携は、起動直後から stdout を JSON-RPC プロトコルとして読み始める。
期待される stdout:
{"jsonrpc":"2.0","id":1,"result":{...}}
実際の stdout:
(starship / zoxide の初期化メッセージ)
{"jsonrpc":"2.0","id":1,"result":{...}}
JSON パーサーは最初の非 JSON 行でエラーを吐き、接続が切れる。しかしエラーは「接続できなかった」として処理され、stdout の中身はログに出ない。原因が全く見えない。
修正:中間シェルをなくす
#!/usr/bin/env bash
HOST_PATH="/usr/bin:/usr/local/bin:/home/rbcn2000/.npm-global/bin:$PATH"
exec flatpak-spawn --host env PATH="$HOST_PATH" \
node /home/rbcn2000/.npm-global/lib/node_modules/@openai/codex/bin/codex.js "$@"
変更点は三つ。
- 中間の zsh をなくした — stdout に余計なものが一切流れない
nodeを直接指定した — シンボリックリンク(codex)経由の shebang 解決をしない- PATH をホスト向けに明示した — Flatpak サンドボックスの PATH には
/usr/binと npm-global が含まれないため、flatpak-spawn --hostに渡す環境を自分で組み立てる
これで動いた。
なぜ Claude Code は最初から動いたのか
余談だが、Claude Code(Claudian のメインの連携先)が最初から問題なく動いた理由も興味深い。答えは、ビルド方式の根本的な違いにある。
Claude Code のバイナリを調べると、こうなっている。
$ file ~/.local/share/claude/versions/2.1.177
ELF 64-bit LSB executable, x86-64, dynamically linked
$ ldd ~/.local/share/claude/versions/2.1.177
librt.so.1, libc.so.6, libpthread.so.0, libdl.so.2, libm.so.6
$ ls -lh ~/.local/share/claude/versions/2.1.177
-rwxr-xr-x 239M claude
外部ライブラリへの依存は libc 系のみ。libnode.so も libv8.so も存在しない。サイズは 239MB。
これは Bun の bun build --compile で生成された standalone ELF の特徴だ。Bun はコンパイル時に JavaScript エンジン(JavaScriptCore)をバイナリに静的バンドルする。結果として、外部の Node.js が一切不要な自己完結バイナリになる。
Claude Code(Bun standalone):
ELF バイナリ
└─ JavaScriptCore(静的バンドル)
└─ アプリケーションコード(静的バンドル)
→ PATH に何が入っていても関係ない
→ Flatpak サンドボックスでも、どこからでも動く
Codex CLI(Node.js スクリプト):
codex.js ← #!/usr/bin/env node
→ 実行時に PATH から node を探す
→ nvm 管理の node は Flatpak の PATH にない
→ 詰まる
一方 Codex CLI は現時点では Node.js スクリプトであり、実行時に node を PATH から探す。Flatpak サンドボックスの PATH は nvm を知らないため、ここで詰まる。
Codex が Rust 製ネイティブバイナリに移行した場合、話は変わる。Rust のバイナリも自己完結であり、外部ランタイムへの依存がない。その時点でこの wrapper 問題はそもそも発生しなくなる。
「Claude Code は Bun、Codex は Rust へ移行中」という話は巷でも語られているが、Flatpak ユーザーにとってはそれが「なぜ片方だけ動いたのか」という実体験に直結している。抽象的なビルドシステムの選択が、サンドボックス環境での動作可否として手元で現れる——というのは、なかなか興味深い体験だった。
なぜコミュニティプラグインは Flatpak を考慮しないのか
二つの問題を経験して、構造的な理由が見えてきた。
① Flatpak の Obsidian ユーザーは少数派だ
Linux デスクトップユーザー全体の中で Obsidian を使う人は多くない。その中で Flatpak 版を選ぶ人はさらに少ない。プラグイン開発者が macOS や Windows、あるいは .deb / AppImage の Linux ユーザーとして開発していれば、Flatpak の挙動に気づくことはない。
② Flatpak の壁は「見えにくい」
command not found や permission denied なら原因を特定しやすい。しかし Flatpak の問題は往々にして「タイムアウト」「サイレントな接続失敗」として現れる。開発者も再現できないため、バグレポートが難しい。
③ プラグインの前提は「シェルと同じ環境」
サブプロセスを起動するプラグインはほぼ例外なく、PATH・環境変数・SSH_AUTH_SOCK などが「ターミナルと同じ値である」と仮定している。Flatpak はその仮定を静かに崩す。
結論:AppImage に移行する
workaround を積み上げるほど、構成は脆くなる。
- SSH agent のために:systemd サービス、wrapper script、
.desktopオーバーライド - Codex CLI のために:
flatpak-spawnwrapper、PATH の手動組み立て
それぞれは正しい解決策だ。しかし組み合わさると、どれか一つの変更が別の何かを壊す可能性がある。nvm のバージョンを上げたら PATH がずれる。systemd を変えたら socket パスが変わる。
AppImage は別のアプローチだ。ホスト環境を一切変えず、アプリケーション単体をファイルとして扱う。サンドボックスを設けないため、PATH も SSH_AUTH_SOCK もターミナルと同じものがそのまま届く。
AppImage の Obsidian:
ホストの PATH → そのまま継承
SSH_AUTH_SOCK → そのまま継承
サブプロセス起動 → ホストの環境で動く
flatpak-spawn → 不要
管理の手間を考えると、AppImage への移行はシンプルな選択に見える。
まとめ
Flatpak 版 Obsidian で生産性を上げようとしたら、二つの大きな壁にぶつかった。
SSH agent:プロセス継承のスコープを理解し、systemd user service で固定することで解決した。
Codex CLI / stdio JSON-RPC:flatpak-spawn --host の使い方と、stdio プロトコルに対して interactive shell を使ってはいけないという教訓を得た。
どちらも「Flatpak が悪い」というわけではない。Flatpak は正しくサンドボックスしている。問題は、その制約を前提としていないプラグインと、その制約を意識せずに使っていた自分にある。
Flatpak sandbox で stdio プロトコルを使うプロセスを起動するとき、interactive shell を中間に挟んではならない。
これが今回の最も具体的な教訓だ。
そして最終的な教訓として:workaround の積み重ねは、ある時点でメンテナンスコストが逆転する。 AppImage への移行は、その逆転点を迎えたサインだ。
cover image: unsplash