Cebimdeki Claude Code: Uzaktan Erişimi Elle Kurmak

iPhone'dan MacBook'taki Claude Code'a Tailscale, sertleştirilmiş OpenSSH, mosh ve tmux ile — adım adım, elle ve tuzaklarıyla birlikte.

tau, kendim için yaptığım kişisel bir ajan platformu: MacBook'ta çalışan bir hub, onunla da Telegram'dan, terminalden ve sesle konuşuyorum. Katman katman kuruyorum; en alttaki, sıfırıncı katmanın ajanla bir ilgisi bile yok. Tek işi, telefondan Mac'teki Claude Code'u — Anthropic'in terminalde çalışan kodlama ajanını — kullanabilmek. Masadan kalktığımda iş de durmasın diye.

Repoda bunu kuran script'ler hazırdı; tmux ve mosh gibi sudo istemeyen kısım da zaten kuruluydu. Şifre, hesap ve telefon isteyen kalanını yine de script'i çalıştırmadan, elle ve adım adım yaptım: ne yaptığını anlamadığım bir kapıyı sonra savunamam. En çok da yol boyunca çıkan tuzaklardan öğrendim.

Zincir

iPhone'da Termius → Tailscale → Mac'te OpenSSH → mosh → tmux → Claude Code. Her halkanın tek bir işi var.

telefondan claude code'a
iPhoneMacBooktailnet · TailscaleSSH · anahtarmosh · UDPbaşlatırtau-attachreddedilirTermiusSSH istemcisisshdUzaktan Giriş · :22mosh-serverUDP oturumutmuxoturum: tauClaude CodeclaudeKafedeki biritailnet dışıOturum2Kapı1İstemci2Ajan1

Yol: Tailscale

Tailscale, cihazları nerede olurlarsa olsunlar aynı yerel ağdaymış gibi birbirine bağlayan bir VPN; bu özel ağa tailnet deniyor. Router'da port açmak gerekmiyor. Mac'e brew install --cask tailscale-app ile kurdum, Gizlilik ve Güvenlik'te sistem uzantısına izin verip giriş yaptım. iPhone'a da kurup aynı hesapla girdim.

İlk karışıklık hemen geldi: Tailscale'i açtığımı sanıp Termius'u açmışım. Termius, iPhone'da kullandığım SSH istemcisi; SSH de başka bir makinede güvenli bir terminal açmanın standart yolu. İkisini kafamda ayıran benzetme şu oldu: Tailscale yol, Termius araba. Tailscale telefonla Mac arasına bir yol döşüyor ama o yolda kendiliğinden hiçbir şey gitmiyor; Mac'e giden şey Termius.

Yolun gerçekten var olduğunu Mac'ten görmek için:

tailscale ping <telefon>

Cevap via <yerel-ip>:41641 diye geldi: iki cihaz birbirini aynı Wi-Fi'da bulmuş, trafik doğrudan, eşten eşe gidiyor. via DERP(...) görseydim trafik Tailscale'in aktarma sunucuları olan DERP üzerinden dolaşıyor olacaktı. Sonra 5G'de de denedim, orada da çalıştı.

Anahtar ve panodaki komut

SSH'ta şifre yerine anahtar çifti kullanıyorum: özel anahtar telefonda kalıyor, açık (public) anahtar Mac'e gidiyor ve Mac yalnızca o özel anahtarı taşıyanı içeri alıyor. Termius'ta Keychain → Generate Key ile bir ED25519 anahtarı ürettim ve public key'i Mac'teki ~/.ssh/authorized_keys dosyasına ekledim:

mkdir -p ~/.ssh && chmod 700 ~/.ssh
echo "$(pbpaste)" >> ~/.ssh/authorized_keys
chmod 600 ~/.ssh/authorized_keys

Kontrol ettim:

ssh-keygen -l -f ~/.ssh/authorized_keys
# ... is not a public key file.

cat ile baktığımda dosyada anahtar değil, komutun kendisi yazıyordu.

echo 'ssh-ed25519 AAAA… iphone-termius' > ~/.ssh/authorized_keys

Tek >, bozuk satırı da silsin diye. Sondaki iphone-termius bir yorum: SSH onu umursamıyor, ama telefon kaybolursa hangi satırı sileceğimi bir bakışta gösteriyor. ssh-keygen -l bu sefer anahtarın parmak izini (fingerprint, SHA256:…) yazdı.

Kapıyı açmadan önce kilitle

macOS'ta SSH sunucusu, yani OpenSSH'ın sshd servisi, Sistem Ayarları'nda Uzaktan Giriş (Remote Login) olarak geçiyor. Açmadan önce sertleştirdim; bir dakikalığına bile varsayılan ayarlarla açık kalmasın istedim.

/etc/ssh/sshd_config içinde Include /etc/ssh/sshd_config.d/* satırı var: o klasördeki dosyalar alfabetik sırayla okunuyor ve çoğu ayarda ilk görülen değer kazanıyor. macOS kendi ayarlarını 100-macos.conf dosyasına koyuyor. Benimki bu yüzden 010-tau.conf: önce okunuyor, sözü önce o söylüyor.

Dosyayı repodaki şablondan sed ile ürettim, install ile yerine koydum:

sed -e 's|__USER__|<kullanıcı>|g' \
    -e 's|__TAILNET_V4__|100.64.0.0/10|g' \
    -e 's|__TAILNET_V6__|fd7a:115c:a1e0::/48|g' \
    infra/remote/sshd/tau.conf > 010-tau.conf
sudo install -m 644 -o root -g wheel 010-tau.conf /etc/ssh/sshd_config.d/010-tau.conf

install, çünkü sshd grubun ya da herkesin yazabildiği bir config dosyasını kabul etmiyor; install sahibi ve izinleri tek adımda doğru koyuyor. İşin özü şu satırlar:

PasswordAuthentication no
KbdInteractiveAuthentication no
AuthenticationMethods publickey
PermitRootLogin no
AllowUsers <kullanıcı>@100.64.0.0/10 <kullanıcı>@fd7a:115c:a1e0::/48 <kullanıcı>@127.0.0.1 <kullanıcı>@::1
ClientAliveInterval 30
ClientAliveCountMax 4
AllowAgentForwarding no
AllowTcpForwarding local

"no hostkeys available"

Kurduktan sonra, Uzaktan Giriş'i açmadan önce test ettim:

sudo sshd -t
# sshd: no hostkeys available -- exiting.

Buradaki anahtar benim anahtarım değil. SSH'ta iki taraf da kimliğini kanıtlıyor. User key benim Mac'e "ben buyum" dememi sağlıyor; Termius'ta ürettiğim buydu. Host key ise sunucunun kimliği: bağlandığım şeyin gerçekten benim Mac'im olduğunu bana kanıtlıyor. Bu Mac'te Uzaktan Giriş hiç açılmamıştı; macOS host key'leri normalde ilk bağlantıda ürettiği için ortada hiç yoktu.

sudo ssh-keygen -A

-A yalnızca eksik host key'leri üretiyor, var olanlara dokunmuyor. Ardından sshd -t sessizce geçti. Repodaki kurulum script'inde de aynı boşluk vardı; onu da düzelttim.

Host key'in bir faydası daha var: ilk bağlantıda Termius onun parmak izini gösterip güvenip güvenmediğini soruyor. Körlemesine "evet" demek yerine Mac'teki değerle karşılaştırmak lazım; ikisi aynıysa bağlandığın makine gerçekten o Mac:

ssh-keygen -l -f /etc/ssh/ssh_host_ed25519_key.pub

Uzaktan Giriş'i açmak

sudo systemsetup -setremotelogin on
# ... requires Full Disk Access privileges.

Terminale Tam Disk Erişimi (Full Disk Access) vermek, o terminalde çalışan her şeye bütün diski açmak demek; tek bir ayar için fazla. Uzaktan Giriş'i Sistem Ayarları → Genel → Paylaşma'dan elle açtım.

Açık kalmış iki kapı daha

Asıl sürpriz o Paylaşma ekranındaydı. Uzaktan Giriş'in yanında iki şey daha açıktı:

netstat -an -p tcp | grep LISTEN

Bu portlar * adresinde dinliyordu, yani her arayüzde, kafenin Wi-Fi'ı dahil. Üstüne macOS giriş şifresini kabul ediyorlar, az önceki sertleştirme onları hiç kapsamıyor (o yalnızca sshd'nin ayarı) ve güvenlik duvarı kapalıydı, çünkü mosh UDP ile çalışıyor. Yani aynı kafe Wi-Fi'ındaki herkes bu servislere şifre deneyebilirdi. İkisini de kapattım; uzaktan erişim kapılarından geriye yalnızca 22 kaldı.

Sağlık kontrolü

doctor.sh, repodaki sağlık kontrolü; hiçbir şeyi değiştirmeden bütün zinciri yokluyor. En sevdiğim kontrolü şu: kendi makinesine şifreyle girmeyi deniyor ve sunucu yalnızca Permission denied (publickey) diyorsa şifrenin gerçekten kapalı olduğuna inanıyor. Config'e "no" yazmakla sunucunun gerçekten reddetmesi aynı şey değil. Hepsi yeşil yandı.

Araba: Termius ve mosh

Termius'ta yeni bir host:

mosh (mobile shell), SSH'ı yalnızca kimlik doğrulamak ve Mac'te mosh-server'ı başlatmak için kullanıyor; sonrasında oturum UDP üzerinden akıyor. Tek bir TCP bağlantısına bağlı olmadığı için Wi-Fi'dan 5G'ye geçmek ya da telefonun uyuması onu öldürmüyor; telefon uyanınca ekran kaldığı yerden devam ediyor.

tmux: oturum Mac'te yaşar

mosh bağlantıyı dayanıklı yapıyor, ama bağlantıyı tamamen kapatırsam terminal de, içinde çalışan Claude Code da gidiyor. tmux, Mac'te bağlantıdan bağımsız yaşayan bir terminal oturumu: ben kopsam da içindekiler çalışmaya devam ediyor, geri bağlanınca aynı ekrana dönüyorum.

Repodaki tau-attach özünde tek satır — tau adlı tmux oturumuna bağlan, yoksa oluştur:

exec tmux new-session -A -s tau

Önce telefondan elle yazdım, ekranın altında tmux'un durum çubuğu belirdi. Sonra Termius'ta host'un Startup Snippet alanına tau-attach yazdım; artık her bağlantı doğrudan çalışan oturuma düşüyor.

Test

5G üzerinden bağlandım, claude yazdım. Yeniden giriş istemedi. (SSH oturumunda macOS'un şifre kasası keychain kilitli görünür de Claude Code giriş isterse diye README'de claude setup-token ile kalıcı bir token yolu yazılı; gerek kalmadı.)

Asıl test: telefonu kilitleyip iki dakikadan fazla beklemek, yani sshd'nin ölü bağlantıları temizlediği süreden uzun. Döndüğümde aynı ekran. Sonra bağlantıyı tamamen kapatıp yeniden bağlandım: yine aynı, hâlâ çalışan Claude oturumu.

Tek eksik: uyku

Kapak kapanınca Mac uyuyor ve uyuyan Mac'e hiçbir şey ulaşamıyor. Şimdilik bir launchd ajanı caffeinate -s çalıştırıyor: caffeinate macOS'un uykuyu engelleyen komutu, -s ile de yalnızca şarjdayken geçerli; pildeyken ve kapak kapalıyken Mac yine uyuyor. Bu işi ileride tau'nun menü çubuğunda yaşayacak tray uygulaması TauBar devralacak.

Güvenlik modeli

Nasıl doğrularsın

  1. tailscale ping <telefon> cevap veriyor: via <ip>:<port> doğrudan, via DERP(...) aktarmalı bağlantı demek.
  2. ssh-keygen -l -f ~/.ssh/authorized_keys her satır için bir parmak izi yazıyor, "is not a public key file" demiyor.
  3. sudo sshd -t hiçbir şey yazmadan dönüyor.
  4. ssh -o BatchMode=yes -o PubkeyAuthentication=no <kullanıcı>@127.0.0.1 true yalnızca Permission denied (publickey) diyor.
  5. netstat -an -p tcp | grep LISTEN çıktısında 5900, 3283 ve 3031 yok.
  6. Termius'un ilk bağlantıda gösterdiği host key parmak izi, ssh-keygen -l -f /etc/ssh/ssh_host_ed25519_key.pub çıktısıyla aynı.
  7. Telefondan bağlan, claude başlat, telefonu iki dakikadan fazla kilitle: aynı ekran. Bağlantıyı tamamen kapatıp yeniden aç: yine aynı oturum.

Sıfırıncı katman bu kadar. Artık Mac başında değilken de Claude Code'la çalışabiliyorum; sırada ajanın kendisi var.

Kendin kurmak istersen: Telefondan Claude Code: Sıfırdan Kurulum Rehberi.