zsh に移行したら Obsidian Git が壊れた
SSH agent と Flatpak の格闘記

はじめに
前の記事で .bashrc をやめ、bash と zsh を役割分担させた。ターミナル環境は整った。
翌日、Obsidian を開いたら Git の同期が止まった。
[email protected]: Permission denied (publickey).
fatal: Could not read from remote repository.
「zsh にしたら Obsidian が壊れた」ように見えた。しかし実際の原因はシェルではなかった。
ssh-agent のスコープが GUI アプリに届いていなかったのだ。
これは、その原因を特定し、Flatpak サンドボックスと格闘しながら解決した記録だ。
問題の構造
ターミナルで確認すると、SSH 鍵は見えている。
echo "$SSH_AUTH_SOCK" # /run/user/1000/gcr/ssh
ssh-add -l # work-github / personal-github
しかし Obsidian Git plugin では毎回パスフレーズを求められる。
Terminal zsh → OK
Obsidian GUI → NG
問題はこう分解できた。
シェル起動の ssh-agent
→ そのシェル配下だけ有効
GUI 起動の Obsidian
→ .zshrc を読まない
→ ssh-agent に届かない
つまり、認証はシェル設定の問題ではなく、ログインセッション全体の設計の問題だった。
なぜ .bashrc 時代は動いていたのか
ここで疑問が生じた。.bashrc に全部盛りしていた頃は、Obsidian Git が動いていた。なぜか。
調べると、GNOME Keyring(gcr)が関係していることが分かった。
gcr-ssh-agent は、GNOME のログインセッションと連携し、login keyring が自動アンロックされるときに SSH 鍵を自動で公開する。つまり、以前は Obsidian がシェルを経由せずとも gcr 経由で鍵を参照できていたのだ。
「パスフレーズが不要だった」のではなく、過去に入力・保存されたパスフレーズを GNOME Keyring が自動的に使っていたのだった。
gcr-ssh-agent 統一を目指した(そして失敗した)
この仕組みを活かし、すべての入口を gcr で統一しようとした。
gcr-ssh-agent
→ /run/user/1000/gcr/ssh
→ Terminal / Editor / Obsidian / Flatpak から共有
Flatpak の Obsidian に gcr socket を見せた。
flatpak override --user \
--filesystem=xdg-run/gcr \
--env=SSH_AUTH_SOCK=/run/user/1000/gcr/ssh \
md.obsidian.Obsidian
確認すると、一時的に動いた。GitHub 認証が通った。
しかし、再起動後に問題が再発した。
署名処理で固まる
ssh-add -l # personal-github / work-github → 見えている
ssh -T github-personal # → 固まる(返答が来ない)
詳細ログを見ると、GitHub は鍵を受け入れていた。
Offering public key: id_ed25519_personal
Server accepts key: id_ed25519_personal
signing using ssh-ed25519 ← ここで固まる
gcr-ssh-agent への署名依頼が返ってこない状態だった。
ssh-add -D で消しても鍵が復活する
ssh-add -D # All identities removed.
ssh-add -l # personal-github / work-github ← 即座に復活
gcr が keyring に保存された情報から鍵を自動公開していた。その自動公開された状態が、署名処理で不安定になっていた。
Seahorse での掃除は危険だった
Seahorse(GNOME のキーリング管理 GUI)で gcr 側の SSH 鍵項目を削除しようとすると、秘密鍵ファイル(~/.ssh/id_ed25519_personal)ごと削除するリスクがあることが分かった。
ここで gcr-ssh-agent 統一を断念した。
未解明の点
gcr-ssh-agent が署名処理で固まる根本原因は、最終的に特定できなかった。
分かったことは以下だ。
鍵ファイルが壊れているわけではない
GitHub 側の公開鍵登録が間違っているわけでもない
Flatpak の socket 公開だけが原因でもない
~/.ssh/config の設定が間違っているわけでもない
→ gcr-ssh-agent 内部の自動復元状態が、署名処理で不安定になっていた
→ 原因は未解明
「gcr-ssh-agent が悪い」という断定ではない。この環境では、この問題が起きた。そのため、決定性を優先して OpenSSH ssh-agent に切り替えることにした。
OpenSSH ssh-agent を systemd user service として固定する
gcr に頼らず、固定した socket を持つ OpenSSH ssh-agent を systemd user で管理することにした。
mkdir -p ~/.config/systemd/user
cat > ~/.config/systemd/user/ssh-agent.service <<'EOF'
[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
EOF
systemctl --user daemon-reload
systemctl --user enable --now ssh-agent.service
socket は /run/user/1000/ssh-agent.socket に固定される。
~/.ssh/config を更新する
IdentityAgent を明示し、SSH_AUTH_SOCK の揺れに依存しない設定にした。
Host github-personal
HostName github.com
User git
IdentityFile ~/.ssh/id_ed25519_personal
IdentitiesOnly yes
AddKeysToAgent yes
IdentityAgent /run/user/1000/ssh-agent.socket
Host github-work
HostName github.com
User git
IdentityFile ~/.ssh/id_ed25519_work
IdentitiesOnly yes
AddKeysToAgent yes
IdentityAgent /run/user/1000/ssh-agent.socket
bash / zsh で socket を正規化する
.bashrc と .zshrc 両方に入れる。
# OpenSSH ssh-agent(.bashrc / .zshrc 共通)
if [ -S "$XDG_RUNTIME_DIR/ssh-agent.socket" ]; then
export SSH_AUTH_SOCK="$XDG_RUNTIME_DIR/ssh-agent.socket"
fi
Flatpak 対応
Flatpak の Obsidian は独自のサンドボックス内で動く。ホスト側の socket を見せる必要がある。
flatpak override --user \
--filesystem=xdg-run/ssh-agent.socket \
--env=SSH_AUTH_SOCK=/run/user/1000/ssh-agent.socket \
md.obsidian.Obsidian
# gcr は外す
flatpak override --user \
--nofilesystem=xdg-run/gcr \
md.obsidian.Obsidian
Obsidian Git wrapper
Obsidian Git plugin が Flatpak 内の /app/bin/git を使うため、git コマンドを wrapper で包んだ。
cat > ~/.local/bin/obsidian-git <<'EOF'
#!/usr/bin/env bash
SOCK="/run/user/1000/ssh-agent.socket"
export SSH_AUTH_SOCK="$SOCK"
export GIT_SSH_COMMAND="ssh \
-F /home/rbcn2000/.ssh/config \
-o IdentityAgent=$SOCK \
-o BatchMode=yes \
-o ConnectTimeout=10 \
-o ServerAliveInterval=5 \
-o ServerAliveCountMax=1"
exec /app/bin/git "$@"
EOF
chmod +x ~/.local/bin/obsidian-git
BatchMode=yes が重要だ。自動コミット・自動同期の裏側でパスフレーズ入力待ちになって固まるのを防ぐ。
Obsidian Git plugin の設定で Custom Git binary path に /home/rbcn2000/.local/bin/obsidian-git を指定する。
再起動後の鍵投入を自動化する
OpenSSH ssh-agent は gcr と違い、鍵を自動復元しない。再起動後は空の状態から始まる。
keychain を戻す選択肢もあったが、agent の二重化リスクがあるため採用しなかった。代わりに「鍵投入だけ」を担う最小のスクリプトを作った。
cat > ~/.local/bin/ssh-load-github <<'EOF'
#!/usr/bin/env bash
set -u
SOCK="/run/user/1000/ssh-agent.socket"
export SSH_AUTH_SOCK="$SOCK"
if [ ! -S "$SOCK" ]; then
systemctl --user start ssh-agent.service 2>/dev/null || true
fi
if [ ! -S "$SOCK" ]; then
echo "ssh-agent socket not found: $SOCK" >&2
exit 1
fi
# 非対話環境ではパスフレーズ入力待ちにしない
if [ ! -t 0 ]; then
exit 0
fi
CURRENT_KEYS="$(ssh-add -l 2>/dev/null || true)"
if ! printf '%s\n' "$CURRENT_KEYS" | grep -q 'personal-github'; then
ssh-add "$HOME/.ssh/id_ed25519_personal" || exit 1
fi
CURRENT_KEYS="$(ssh-add -l 2>/dev/null || true)"
if ! printf '%s\n' "$CURRENT_KEYS" | grep -q 'work-github'; then
ssh-add "$HOME/.ssh/id_ed25519_work" || exit 1
fi
EOF
chmod +x ~/.local/bin/ssh-load-github
.zshrc と .bashrc で interactive shell 起動時に呼び出す。
if [[ \(- == *i* ]] && [ -t 0 ] && [ "\){SSH_LOAD_GITHUB_ON_SHELL:-1}" = "1" ]; then
"$HOME/.local/bin/ssh-load-github" 2>/dev/null || true
fi
これで、ターミナルを最初に開いたとき、鍵が未ロードであればパスフレーズを1回入力するだけでよくなった。
.desktop 起動の落とし穴と GUI ランチャー
これで解決したと思ったが、もうひとつ問題があった。
再起動後、Obsidian を最初に起動すると Git が動かない。
原因は ssh-load-github の TTY チェックだ。
if [ ! -t 0 ]; then
exit 0 # TTY がなければ何もしない
fi
.desktop ファイルから起動する場合、TTY がない。そのため ssh-load-github が即座に終了し、鍵が未ロードのまま Obsidian が起動してしまう。
解決策:SSH_ASKPASS でGUI ダイアログを使う
TTY なし環境でパスフレーズ入力を受け付けるための仕組みが OpenSSH に用意されている。SSH_ASKPASS だ。
sudo apt install ssh-askpass-gnome
これをラップする Obsidian 専用のランチャーを作った。
cat > ~/.local/bin/open-obsidian <<'EOF'
#!/usr/bin/env bash
SOCK="/run/user/1000/ssh-agent.socket"
export SSH_AUTH_SOCK="$SOCK"
if [ ! -S "$SOCK" ]; then
systemctl --user start ssh-agent.service 2>/dev/null || true
fi
if [ ! -S "$SOCK" ]; then
echo "ssh-agent socket not found: $SOCK" >&2
exit 1
fi
# TTY がなくても GTK ダイアログでパスフレーズ入力を受け付ける
export SSH_ASKPASS="/usr/lib/openssh/gnome-ssh-askpass"
export SSH_ASKPASS_REQUIRE=force
CURRENT_KEYS="$(ssh-add -l 2>/dev/null || true)"
if ! printf '%s\n' "$CURRENT_KEYS" | grep -q 'personal-github'; then
ssh-add "$HOME/.ssh/id_ed25519_personal" || exit 1
fi
CURRENT_KEYS="$(ssh-add -l 2>/dev/null || true)"
if ! printf '%s\n' "$CURRENT_KEYS" | grep -q 'work-github'; then
ssh-add "$HOME/.ssh/id_ed25519_work" || exit 1
fi
exec flatpak run md.obsidian.Obsidian "$@"
EOF
chmod +x ~/.local/bin/open-obsidian
.desktop ファイルをオーバーライドする
Flatpak のシステム側 .desktop は直接編集できない(更新で上書きされる)。ユーザー領域にコピーして差し替える。
cp /var/lib/flatpak/exports/share/applications/md.obsidian.Obsidian.desktop \
~/.local/share/applications/md.obsidian.Obsidian.desktop
sed -i 's|^Exec=.*|Exec=/home/rbcn2000/.local/bin/open-obsidian %U|' \
~/.local/share/applications/md.obsidian.Obsidian.desktop
update-desktop-database ~/.local/share/applications/
これで、アプリランチャーから Obsidian を開くと、まず GTK のパスフレーズダイアログが表示され、入力後に Obsidian が起動する。
最終的な構造
systemd user
↓
OpenSSH ssh-agent.service
↓
/run/user/1000/ssh-agent.socket
├─ bash / zsh(起動時に ssh-load-github)
├─ ~/.ssh/config(IdentityAgent で明示)
└─ ~/.local/bin/open-obsidian ← .desktop から起動
└─ SSH_ASKPASS=gnome-ssh-askpass(GTK ダイアログ)
↓
Flatpak Obsidian
↓
~/.local/bin/obsidian-git(BatchMode=yes)
↓
/app/bin/git
再起動後の挙動はこうなった。
ターミナルを最初に開く場合:
→ ssh-load-github 起動
→ パスフレーズを1回入力
→ 以降、bash / zsh / Obsidian すべてで共有
Obsidian を最初に開く場合:
→ open-obsidian 起動
→ GTK ダイアログでパスフレーズ入力
→ 以降、bash / zsh / Obsidian すべてで共有
責務の分離
最終的に、ssh-agent まわりの責務がこう整理できた。
| 役割 | 担当 |
|---|---|
| agent 起動 | systemd user ssh-agent.service |
| agent 選択 | SSH_AUTH_SOCK / IdentityAgent |
| 鍵投入(ターミナル) | ssh-load-github(TTY あり) |
| 鍵投入(GUI 起動) | open-obsidian + SSH_ASKPASS(TTY なし) |
| 自動 Git 処理 | BatchMode=yes |
keychain のような「まとめてやってくれるツール」を使うと便利だが、agent 二重化のリスクや GUI アプリへの不到達が起きやすい。責務を分けることで、どこで何が起きているかを把握できるようになった。
教訓
環境変数は「設定」ではなく「プロセス継承の結果」である。
SSH_AUTH_SOCK が正しく設定されていても、それを持つプロセスから起動されなければ子プロセスには届かない。シェルの .zshrc に書いた設定は、GUI アプリには届かない。これがすべての根本だった。
そして追加の教訓として:
便利な GUI 統合より、固定された socket と単純な agent の方が決定性が高い場合がある。
gcr-ssh-agent はデスクトップとの統合が美しい。しかし今回の環境では、その自動復元の仕組みが署名処理で不安定になった。シンプルな OpenSSH ssh-agent を固定 socket で起動する方が、ずっと予測可能だった。


