להריץ Claude Code ב-VPS עם tmux
למנוע קטיעת סשיית Claude Code בעת ניתוק SSH, יש להריץ את ה-CLI בתוך tmux על שרת Linux. נלמד איך להתקין, להגן על השרת ולמנוע שגיאות SIGHUP.
הבעיה היא מכסה הלפטופ, לא ה-CLI
Claude Code עובד כשורה על הלפטופ עד לסגירת המכסה: חיבור ה-SSH נקטע, ה-shell מקבל SIGHUP, וה-agent מפסיק לפעול באמצע הרצת בדיקה. הרץ את ה-CLI על מכשיר שאינו נכנס למצב שינה, בתוך terminal multiplexer שבו התהליכים אינם תהליכי בן (children) של סשיית ה-SSH שלך. זהו המפתח — ו-tmux, ולא תהליך ההתקנה, הוא המרכיב הקריטי.
עמוד זה עוסק בהפעלה של שרת שבו מריצים agents. אם אין לך שרת Linux שניתן להשאיר פועל, המידע אינו רלוונטי עבורך. זוהי דרישת הקדם היחידה.
מה tmux באמת עושה
כשמתחברים ב-SSH, הפקודה sshd יוצרת תהליך shell חדש ומקצה לו pseudo-terminal; כל תהליך שמתחיל מתוך ה-shell הזה הוא תהליך בן (child) שלו. אם החיבור נقطع, ה-kernel סוגר את ה-pty, ה-shell מקבל SIGHUP, והוא מנתק את הקשר עם תהליכי ה-child שלו. תהליכים המורצים ב-foreground לאורך זמן יסיימו את פעולתם.
tmux הופך את מבנה הבעלות. הפקודה tmux שאתה מקליד היא client דק המתקשר דרך unix socket אל tmux server הפועל בנפרד מהטרמינל שלך. ה-shells בתוך session הם תהליכי בן של ה-server הזה, ולא של sshd. אם תנתק את חיבור ה-SSH, ה-client ייעלם, אך ה-server, ה-session וה-agent ימשיכו לפעול גם באמצע המשימה. אם תתחבר מחדש, tmux attach, תחזור לאותו shell עם אותו היסטוריית פקודות (scrollback). גם nohup שורד ניתוק, אך הוא לא מאפשר חזרה — לא ניתן לבצע re-attach ל-TUI שרץ ברקע. Claude Code הוא אינטראקטיבי; tmux (או screen) הוא הכלי המתאים.
Sizing the box
ה-CLI הוא תהליך Node; הוא אינו מה שממלא את המכונה. מה שממלא את המכונה הוא כל מה שה-agent מריץ עבורך: build, סדרת בדיקות מלאה, tsc, language server, או מסד נתונים בתוך Docker. בצע sizing עבור ה-toolchain, לא עבור ה-CLI. הוסף swap גם אם אין לך כוונה להשתמש בו — הוא הופך OOM kill חריף לתהליך build איטי:
sudo fallocate -l 4G /swapfile
sudo chmod 600 /swapfile
sudo mkswap /swapfile
sudo swapon /swapfile
echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstabעקוב גם אחרי הדיסק: repos, node_modules ו-Docker images צוברים נפח במהירות. ואם ה-toolchain משתמש מעבר ל-containers ומשתמש במכונות וירטואליות מלאות — כמו KVM guest או Kubernetes node מקומי — וודא שהתוכנית שלך כוללת הרשאות CPU virtualisation לפני ההזמנה, מכיוון שrunning nested virtualization on a VPS הוא פיצ'ר שהספק מאפשר עבורך, ולא משהו שניתן להפעיל מתוך ה-guest.
משתמש שאינו root תחילה
צור משתמש ייעודי עם home directory משלו, והצב את המפתח הציבורי במקומו:
sudo adduser --disabled-password --gecos "" agent
sudo install -d -m 700 -o agent -g agent /home/agent/.ssh
sudo cp ~/.ssh/authorized_keys /home/agent/.ssh/authorized_keys
sudo chown agent:agent /home/agent/.ssh/authorized_keys
sudo chmod 600 /home/agent/.ssh/authorized_keysבכוונה, agent אינו נמצא בקבוצת sudo. אם נדרש חבילת מערכת, התקן אותה. החלטה זו מסירה את רוב הדרכים שבהן פקודת shell שגויה עלולה להרוס את ה-host.
SSH hygiene for a box you leave running
אימות באמצעות סיסמה (Password auth) במכונה המחוברת לאינטרנט הציבורי לאורך כל היום, המכילה agent וקוד מקור, היא סיכון שאינו כדאי. כבו את האפשרות הזו. ב-Ubuntu 24.04 ו-Debian 13, /etc/ssh/sshd_config כולל את /etc/ssh/sshd_config.d/*.conf, לכן עדיף להוסיף קובץ חדש במקום לערוך את קובץ הקונפיגורציה הראשי:
# /etc/ssh/sshd_config.d/10-hardening.conf
PasswordAuthentication no
KbdInteractiveAuthentication no
PermitRootLogin noבדקו וטעןו מחדש — השאירו את הסשן הנוכחי פתוח בזמן שאתם בודקים סשן חדש מטרמינל שני:
sudo sshd -t && sudo systemctl restart sshפרט טכני ב-Ubuntu 24.04: ה-sshd מופעל באמצעות socket-activated. הגדרות אימות תקפות ב-systemctl restart ssh, אך שינוי ב-Port מחייב גם systemctl daemon-reload והפעלה מחדש של ssh.socket.
לאחר מכן, חומת האש (firewall). אפשרו SSH לפני שתפעילו אותו, אחרת תאבדו גישה למכונה:
sudo ufw allow OpenSSH
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw enableהתקינו את fail2ban תוך הבנה של התועלת: ברגע שאימות הסיסמה כבוי, התקפות brute force לא יצליחו ממילא — הדבר מונע רישום של ניסיונות כושלים ב-journal.
# /etc/fail2ban/jail.local
[sshd]
enabled = true
backend = systemd
maxretry = 5
bantime = 1hלבסוף, בצעו עדכונים אוטומטיים באמצעות sudo apt install unattended-upgrades ו-sudo dpkg-reconfigure -plow unattended-upgrades. שימו לב לקשר עם tmux: אם Unattended-Upgrade::Automatic-Reboot מופעל ועדכון kernel מתבצע, המכונה תופעל מחדש ותסגור את כל הסשנים. השאירו את האפשרות כבויה ובצעו הפעלה מחדש במועד שתבחרו, כאשר שום תהליך לא נמצא באמצע ריצה.
התקנת Node.js ו-Claude Code ב-Ubuntu
Claude Code הוא ממשק שורת פקודה (CLI) מבוסס Node, לכן יש צורך בגרסה עדכנית של Node. החבילה מהמאגר הרשמי של ההפצה (distro) לרוב אינה מעודכנת מספיק; ב-Ubuntu ו-Debian הדרך המקובלת היא שימוש ב-NodeSource, המספק מאגר חתום (ללא apt-key — הכלי הזה אינו קיים יותר):
curl -fsSL https://deb.nodesource.com/setup_24.x | sudo -E bash -
sudo apt install -y nodejs
node --versionכעת החלק שמשתמשים נוטים לטעות בו: התקינו את ה-CLI תחת המשתמש agent שלכם, לעולם לא עם sudo npm -g. שימוש ב-prefix גלובלי בבעלות root יגרום לשגיאות הרשאות בהמשך וישאיר קבצים בבעלות root בתוך ה-npm cache. הגדירו את ה-prefix של npm לתיקיית הבית של המשתמש תחילה:
mkdir -p ~/.npm-global
npm config set prefix ~/.npm-global
echo 'export PATH="$HOME/.npm-global/bin:$PATH"' >> ~/.bashrc
source ~/.bashrc
npm install -g @anthropic-ai/claude-code
claude --versionפקודת ה-export צריכה להיכתב בתוך ~/.bashrc ולא בתוך ~/.profile, והיא חייבת להופיע מעל לבדיקת ה-"If not running interactively, don't do anything" שנמצאת בחלק העליון של הקובץ: tmux עשוי להפעיל non-login shells, אשר קוראים את ~/.bashrc ומדלגים על ~/.profile — ~/.profile רץ רק עבור login shells. שימוש ב-Node מותאם אישית למשתמש באמצעות מנהל גרסאות כמו nvm משיג תוצאה דומה; המטרה בכל מקרה היא ש-npm install -g לעולם לא יזדקק ל-sudo. npm עדיין יעבוד כראוי, או שניתן להשתמש בסكريפט ההתקנה המקורי של Anthropic, שהוא ברירת המחדל המתועדת כעת. בדקו את מסמכי ההתקנה של Anthropic לפני ההעתקה — שיטות ההתקנה משתנות.
הריצו את claude בתוך ה-repo כדי להתחיל. ההרצה הראשונה תדריך אתכם בתהליך האימות; למחשב ללא מסך (headless) אין דפדפן, לכן התהליך יספק לכם URL לפתיחה במחשב האישי שלכם וקוד להזנה בחזרה בטרמינל. (שימוש ב-API key במשתני הסביבה הוא דרך נוספת.) בכל מקרה, האימות נשמר כעת בשרת — מה שמוביל לחלק שמשתמשים נוטים לדלג עליו.
שיחת ה-blast radius
סוכן (agent) עם גישת shell הוא shell. הוא יכול לקרוא כל דבר שהמשתמש שמריץ אותו יכול לקרוא, ולדחוף (push) לכל מקום שהמשתמש יכול לדחוף אליו. זו אינה ביקורת על הכלי, אלא הגדרה שלו — וזו הסיבה שהחשבון שבו הוא רץ חשוב יותר מכל הגדרה בודדת.
- משתמש ייעודי ללא הרשאות (unprivileged). ללא קבוצת
sudo, וללא ספריית בית (home directory) משותפת לחשבון שלך. - ללא פרטי גישה (credentials) של סביבת הייצור על המכונה. ללא
~/.aws/credentialsהמחזיקה מפתחות ייצור, ללא.envשהועתקו מייצור, וללא סיסמת מסד נתונים עם הרשאת כתיבה לכל דבר קריטי. תן לסוכן פרטי גישה של סביבת staging או הרשאות קריאה בלבד. - טוקנים מוגבלים (Scoped tokens). טוקן GitHub בעל הרשאות מפורטות המוגבל למאגר (repository) אחד; deploy key כאשר גישת קריאה בלבד מספיקה.
Claude Code כולל דגל (flag) שמדלג לחלוטין על בקשות האישור שלו. בלפטופ, בפרויקט זמני, ההחלטה היא שלך. בשרת המכיל טוקנים, הדגל מסיר את המחסום האחרון שעומד בין הוראה שגוית לבין git push --force. מה הדגל משנה בפועל, וכיצד ניתן להגביל סוכן הרץ איתו — החל מהארגז החול (sandbox) המובנה ועד ל-VPS חד-פעמי — מפורט בrunning Claude Code safely on a server.
Deploy key לעומת SSH agent forwarding
קל לפתות אתכם להשתמש ב-ssh -A כדי ש-git יוכל להשתמש במפתח שבלפטופ שלכם. הבינו מה זה מעניק: agent forwarding חושף את ה-socket של ה-SSH agent המקומי שלך לתהליכים הרצים תחת אותו משתמש על המכונה. כל מה שרץ תחת agent — כולל הסוכן — יכול לבקש מהמפתח שלך לחתום עבור כל host שהוא יכול להגיע אליו, כל עוד אתם מחוברים. זה הרבה יותר מ"לאפשר ל-git לבצע pull למאגר אחד בלבד".
במקום זאת, צרו מפתח על השרת, רשמו אותו כ-deploy key ייעודי למאגר (הרשאת כתיבה רק אם הסוכן צריך לבצע push), והגדירו git identity כדי ש-commits מהמכונה יהיו ניתנים לזיהוי:
ssh-keygen -t ed25519 -C "agent deploy key" -f ~/.ssh/id_ed25519_repo
cat ~/.ssh/id_ed25519_repo.pub # paste into the repo's Deploy Keys
git config --global user.name "Agent (build box)"
git config --global user.email "agent@example.com"The tmux workflow
התקן את התוכנה (sudo apt install tmux), ולאחר מכן הגדר ~/.tmux.conf מינימלית:
set -g mouse on
set -g history-limit 50000
set -g default-terminal "tmux-256color"ארבע פקודות מכסות שימוש יומיומי:
tmux new -A -s claude # attach to session "claude", creating it if absent
# ...run `claude` inside it, work normally...
# Ctrl-b then d -> detach; everything keeps running
tmux ls # list sessions
tmux attach -t claude # reattach, from this machine or any other
tmux kill-session -t claudeיש לשנן את tmux new -A -s claude — הפקודה מתחברת לסשן קיים או יוצרת חדש אם הוא לא קיים. כך פקודה אחת מכסה גם את תחילת העבודה וגם את המשך העבודה לאחר ניתוק. מומלץ להגדיר לה alias. בתוך סשן, Ctrl-b c פותח חלון, Ctrl-b n ו-Ctrl-b p עוברים בין חלונות, ו-Ctrl-b [ נכנס למצב העתקה (copy mode) כדי לגלול לאחור (q יוצא ממנו).
נקודה אחת שחשוב לדעת לגבי סשנים שאינם נסגרים: ה-agent שולח מחדש את כל השיחה בכל סבב. קרא את מה סשן Claude Code פעיל צורך בטוקנים שלו לפני שתשאיר סשן פתוח למשך שבוע.
Failure modes
"My session is gone." tmux ls מדפיס no server running on /tmp/tmux-1000/default. זה כמעט תמיד אומר שהתהליך מעולם לא היה בתוך tmux — התחברת ב-SSH, הרצת את claude ישירות, והניתוק הרג את התהליך. אין מה לשחזר. ההרגל שמונע זאת: tmux new -A -s <project> הוא הפקודה הראשונה לאחר כל התחברות.
החלונית (pane) מצטמצמת לריבוע קטן. tmux קובע את גודל הסשן לפי הלקוח (client) המחובר הקטן ביותר. לכן, לקוח ישן שעדיין מחובר ממכונה אחרת דוחק את התצוגה. כדי להוציא את האחרים בכוח בעת ההתחברות, השתמש ב: tmux attach -d -t claude.
Build מדפיס Killed. מילה אחת, ללא stack trace. בדוק באמצעות sudo dmesg -T | grep -i -E 'out of memory|killed process' — ה-OOM killer של ה-kernel בחר בתהליך הגדול ביותר. ב-Node ייתכן שתראה במקום זאת את FATAL ERROR: Ineffective mark-compacts near heap limit Allocation failed - JavaScript heap out of memory. פתרונות, לפי סדר: הוסף swap (מופיע לעיל), הגבל את ה-test ואת ה-parallelism של ה-compiler, הגדל את ה-heap של Node באמצעות NODE_OPTIONS=--max-old-space-size=..., או שדרג את ה-VPS. ה-OOM killer עשוי לבחור ב-tmux server במקום ב-build, מה שיגרום לאובדן הסשן; אם systemd-oomd רץ, הוא עלול להרוג slice של משתמש שלם עם אותה תוצאה.
npm error code EACCES / permission denied, mkdir '/usr/lib/node_modules/...'. התקנה גלובלית לתוך prefix בבעלות root. השתמש ב-prefix ~/.npm-global המופיע לעיל. אם הרצת כבר את sudo npm בנקודה כלשהי, ייתכן שתראה גם את Your cache folder contains root-owned files — תקן באמצעות sudo chown -R $(id -u):$(id -g) ~/.npm.
claude: command not found — אך רק לעיתים. ה-export של ה-PATH שלך נמצא בתוך ~/.bashrc מתחת להגנה "If not running interactively, don't do anything", ולכן shells לא-אינטראקטיביים מדלגים עליו. העבר את ה-export מעל ההגנה הזו ושמור אותו ב-~/.bashrc, לא ב-~/.profile: tmux עשוי להפעיל non-login shells, которые קוראים את ~/.bashrc ולא נוגעים ב-~/.profile.
צבעים משובשים לאחר חיבור. חוסר התאמה ב-TERM — השורה default-terminal לעיל היא הפתרון.
סשנים נעלמים לאחר reboot. זה לא באג: ה-tmux server הוא תהליך, ו-reboot מסתיים אותו. בדוק את uptime.
מה נשבר כשהיקף העבודה גדל
יותר פרויקטים. מומלץ להקצות tmux session אחד לכל repo, בשמו; tmux ls יהיה אז ה-dashboard שלך. אם לא תשמור על משמעת בשמות, תקבל sessions מסוג 0, 1, 2. גם הפורטים (ports) מתפזרים באותו אופן — כאשר שישה repos זקוקים ל:3000, זה הזמן להפסיק להקצות אותם ידנית ולהשתמש ב-Traefik reverse proxy שמנתב מספר אפליקציות תחת Docker Compose כדי לבצע את הניתוב לפי hostname.
יותר משתמשים. tmux sockets הם ברמת המשתמש (per-user), לכן שני מפתחים על אותה מכונה יקבלו שרת tmux נפרד ולא יוכלו לראות את ה-sessions של זה. שיתוף session אחד דרך socket משותף אומר שכולם מקלידים לאותו shell תחת אותו משתמש Unix, מה שיוצר השלכות של הרשאות וביקורת (audit). הפתרון הנכון והפשוט הוא שימוש במשתמשים נפרדים.
עבודה ללא השגחה. tmux מיועד ל-interactive sessions שמתחברים אליהם. משימות שרצות על פי לוח זמנים ללא השגחה צריכות להיות בתוך systemd unit ו-timer. כך הן מקבלות לוגים (logging), מדיניות הפעלה מחדש (restart policy) ויכולת שרידות לאחר boot ללא מאמץ. שימוש ב-tmux עבור משימה מסוג cron מעיד על כך שהמשימה צריכה להיות service.
הערה אחרונה: יש לקשר (bind) שרתי פיתוח כך שהסוכן יתחבר ל-127.0.0.1 ולא ל-0.0.0.0, ולהגיע אליהם דרך SSH tunnel (ssh -L 3000:127.0.0.1:3000 agent@your-server) במקום לפתוח פורטים ב-ufw. ברגע שאתה מעביר (forwarding) חצי מיליון פורטים, או כשמכשיר נייד ומחשב נייד זקוקים לאותו preview, עדיף להשתמש ב-WireGuard VPN המארח עצמית ב-VPS לפני הכל: שרתי הפיתוח יקשרו ל-interface פרטי, ו-ufw ימשיך לחסום כל תעבורה מה-interface הציבורי. חומת אש (firewall) עוזרת רק אם מפסיקים ליצור בה חורים.
Claude Code אינו הבחירה היחידה: הרצת coding AI agent ב-VPS כוללת גם את Aider ו-Goose.
FAQ
האם Claude Code ממשיך לרוץ לאחר ניתוק חיבור ה-SSH שלי?
רק אם הפעלת אותו בתוך tmux. תהליך שהופעל ישירות מתוך ה-SSH shell הוא תהליך בן (child) של ה-shell, והוא נסגר יחד עם ה-pty ברגע שהקישור מתנתק. בתוך tmux ה-shell שייך ל-tmux server המנותק, לכן הסוכן (agent) ממשיך לעבוד באמצע המשימה ו-tmux attach מחזיר אותך לאותו היסטוריית פקודות (scrollback). הפוך את tmux new -A -s <project> לפקודה הראשונה לאחר כל התחברות והבעיה תיעלם.
האם כדאי לי להתקין את ה-CLI באמצעות sudo npm install -g?
לא. שימוש ב-global prefix בבעלות root יגרום ל-EACCES שגיאות בהתקנות עתידיות ולקבצים ב-npm cache בבעלות root. הגדר את ה-prefix של npm ל-~/.npm-global (או השתמש במנהל גרסאות כמו nvm), התקן כמשתמש agent ללא הרשאות root, וייצא את ~/.npm-global/bin אל PATH מתוך ~/.bashrc, מעל ה-interactive guard. אם כבר הרצת את sudo npm פעם אחת, תקן את ה-cache באמצעות sudo chown -R $(id -u):$(id -g) ~/.npm.
האם agent forwarding של ssh -A בטוח על מכונה שמריצה agent?
זה מעניק הרבה יותר ממה שהמשימה זקוקה לו. Forwarding חושף את ה-socket של ה-SSH agent המקומי שלך לכל תהליך שרץ תחת המשתמש הזה, לכן כל דבר על המכונה יכול לבקש מהמפתח שלך לחתום עבור כל host שהוא יכול להגיע אליו כל עוד אתה מחובר. צור מפתח ed25519 על השרת ורשם אותו כ-deploy key לכל מאגר (repository), עם הרשאת כתיבה בלבד אם הסוכן באמת נדרש לבצע push.
מדוע ה-build שלי פשוט מדפיס Killed?
מילה אחת ללא stack trace מעידה על ה-kernel OOM killer. ודא זאת באמצעות sudo dmesg -T | grep -i -E 'out of memory|killed process'; מתוך Node עשוי להופיע JavaScript heap out of memory במקום. בצע את התיקונים לפי הסדר: הוסף swapfile, הגבל את ה-parallelism של ה-test וה-compiler, הגדל את NODE_OPTIONS=--max-old-space-size=..., ולאחר מכן שדרג את ה-VPS. שים לב שה-OOM killer יכול לבחור ב-tmux server במקום ב-build, מה שיגרום לאובדן כל הסשן שלך.
tmux או systemd service?
tmux מתאים לסשנים אינטראקטיביים שאתה מתחבר אליהם, עוקב אחריהם ומקליד בתוכם, וזה בדיוק מה שסשן סוכן (agent session) דורש. עבודה שרצה על סדר פעולות קבוע (schedule) ללא מעקב אנושי צריכה להיות בתוך systemd unit ו-timer, שם ה-logging, מדיניות ה-restart והישרדות לאחר boot ניתנים כברירת מחדל. אם אתה משתמש ב-tmux כדי להריץ משימה בסגנון cron, המשימה צריכה להיות service.