はじめに

しばらく前から、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_SOCKBatchMode=yes を明示する。

詳細は別記事に書いた: zsh に移行したら Obsidian Git が壊れた話 — SSH agent と Flatpak の格闘記


第二の壁:Codex CLI と stdio JSON-RPC

SSH agent を解決して、しばらく安定していた。次に問題が出たのは AI プラグインとの連携だった。

Obsidian で AI コーディング補助を使うとき、プラグインによって実装が大きく異なる。

プラグイン + 連携通信方式CLI の実体Flatpak 影響
Smart Composer + CodexHTTP API(REST)ネットワークは通る → 問題なし
Claudian + Claude Codestdio JSON-RPCBun standalone ELF(自己完結バイナリ)最初から動いた
Claudian + Codex CLIstdio JSON-RPCNode.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 "$@"

変更点は三つ。

  1. 中間の zsh をなくした — stdout に余計なものが一切流れない
  2. node を直接指定した — シンボリックリンク(codex)経由の shebang 解決をしない
  3. 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.solibv8.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 foundpermission denied なら原因を特定しやすい。しかし Flatpak の問題は往々にして「タイムアウト」「サイレントな接続失敗」として現れる。開発者も再現できないため、バグレポートが難しい。

③ プラグインの前提は「シェルと同じ環境」

サブプロセスを起動するプラグインはほぼ例外なく、PATH・環境変数・SSH_AUTH_SOCK などが「ターミナルと同じ値である」と仮定している。Flatpak はその仮定を静かに崩す。


結論:AppImage に移行する

workaround を積み上げるほど、構成は脆くなる。

  • SSH agent のために:systemd サービス、wrapper script、.desktop オーバーライド
  • Codex CLI のために:flatpak-spawn wrapper、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-RPCflatpak-spawn --host の使い方と、stdio プロトコルに対して interactive shell を使ってはいけないという教訓を得た。

どちらも「Flatpak が悪い」というわけではない。Flatpak は正しくサンドボックスしている。問題は、その制約を前提としていないプラグインと、その制約を意識せずに使っていた自分にある。

Flatpak sandbox で stdio プロトコルを使うプロセスを起動するとき、interactive shell を中間に挟んではならない。

これが今回の最も具体的な教訓だ。

そして最終的な教訓として:workaround の積み重ねは、ある時点でメンテナンスコストが逆転する。 AppImage への移行は、その逆転点を迎えたサインだ。

cover image: unsplash