تشغيل Claude Code على خادم VPS بعيد مع tmux
شغّل Claude Code على خادم Linux VPS دائم التشغيل داخل tmux لتنجو جلسات الوكيل من انقطاع اتصال SSH. التثبيت والتحصين وأوضاع الفشل التي عليك توقّعها.
المشكلة في غطاء الحاسوب المحمول، لا في الـ CLI
يعمل Claude Code على حاسوبك المحمول دون مشاكل إلى أن تُغلق غطاءه: تموت جلسة SSH، ويتلقى الـ shell إشارة SIGHUP، ويموت معه الوكيل (agent) الذي أمضى ثلاث دقائق في تشغيل الاختبارات. شغّل الـ CLI على جهاز لا ينام أبدًا، داخل مُضاعِف طرفيات (terminal multiplexer) لا تكون عملياته عمليات فرعية تابعة لجلسة SSH الخاصة بك. هذه هي الحيلة كلها — وtmux، لا عملية التثبيت، هو الجزء الذي يقع عليه العبء الحقيقي.
هذه صفحة عن إدارة خادم تترك الوكلاء يعملون عليه. إن لم يكن لديك خادم Linux يمكنك تركه مشغَّلًا، فلا شيء مما هنا ينطبق عليك. هذا هو الشرط المسبق الحقيقي الوحيد.
ما الذي يفعله tmux فعلًا
عندما تتصل عبر SSH، ينشئ sshd عملية shell ويسلّمها طرفية زائفة (pseudo-terminal)؛ وكل ما تشغّله من ذلك الـ shell يكون عملية فرعية تابعة له. اقطع الاتصال فتهدم النواة الـ pty، ويتلقى الـ shell إشارة SIGHUP، فيُغلق الخط بدوره في وجه عملياته الفرعية. العمليات الأمامية طويلة التشغيل تموت.
يقلب tmux علاقة الملكية هذه. أمر tmux الذي تكتبه ليس سوى عميل خفيف يتخاطب عبر مقبس unix (socket) مع خادم tmux يعمل منفصلًا عن طرفيتك. عمليات الـ shell داخل الجلسة عمليات فرعية تابعة لذلك الخادم، لا لـ sshd. اقطع اتصال SSH فيختفي العميل بينما يواصل الخادم والجلسة والوكيل الذي في منتصف مهمته العمل. أعد الاتصال، ثم tmux attach، فتعود إلى الـ shell نفسه ومعه سجلّ التمرير (scrollback) نفسه. يصمد nohup أمام قطع الاتصال أيضًا، لكنه لا يمنحك طريقًا للعودة — فلا يمكنك إعادة الاتصال بواجهة نصية تفاعلية (TUI) وضعتها في الخلفية. Claude Code تفاعلي؛ وtmux (أو screen) هو الأداة الصحيحة.
تحديد حجم الخادم
الـ CLI عملية Node؛ وليست هي ما يملأ الجهاز. ما يملأ الجهاز هو كل ما يشغّله الوكيل نيابةً عنك: عملية بناء، أو حزمة اختبارات كاملة، أو tsc، أو خادم لغة، أو قاعدة بيانات داخل Docker. حدّد الحجم بناءً على سلسلة الأدوات، لا على الـ CLI. أضف مساحة swap حتى لو كنت تنوي ألّا تلمسها أبدًا — فهي تحوّل قتلًا فوريًا بسبب نفاد الذاكرة (OOM) إلى عملية بناء بطيئة:
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راقب القرص أيضًا: فالمستودعات وnode_modules وصور Docker تتراكم بسرعة. وإذا امتدت سلسلة الأدوات إلى ما بعد الحاويات نحو أجهزة افتراضية كاملة — نظام ضيف على KVM، أو عقدة Kubernetes محلية — فتأكد قبل الالتزام بالخطة من أنها تكشف امتدادات المحاكاة الافتراضية في المعالج، لأن تشغيل المحاكاة الافتراضية المتداخلة على VPS أمر يفعّله المزوّد لك، لا شيء تشغّله أنت من داخل النظام الضيف.
مستخدم غير root أولًا
أنشئ مستخدمًا مخصّصًا له مجلد منزل خاص به، وضع مفتاحك العام في مكانه:
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 شارد أن يخرّب المضيف.
تحصين SSH لخادم تتركه يعمل باستمرار
المصادقة بكلمة المرور على جهاز مكشوف على الإنترنت العام طوال اليوم، وفيه وكيل وشيفرتك المصدرية، ليست خطرًا يستحق أن تتحمّله. عطّلها. على 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.
ثم جدار الحماية. اسمح بـ 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 وستُعيد أي ترقية للنواة تشغيل الخادم، آخذةً معها كل الجلسات. اتركه معطّلًا وأعد التشغيل وفق جدولك أنت، حين لا يكون شيء في منتصف التنفيذ.
تثبيت Node.js وClaude Code على Ubuntu
Claude Code أداة CLI مبنية على Node، لذا تحتاج إلى إصدار حديث من Node. حزمة التوزيعة كثيرًا ما تتأخر؛ وNodeSource هو الطريق المعتاد على Ubuntu وDebian، وهو يوفّر مستودعًا موقَّعًا (من دون 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. البادئة العامة (global prefix) المملوكة لـ root تنتج أخطاء أذونات لاحقًا وتترك ملفات مملوكة لـ root في ذاكرة التخزين المؤقت (cache) الخاصة بـ npm. وجّه بادئة 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 جلسات shell ليست جلسات تسجيل دخول (non-login)، وهذه تقرأ ~/.bashrc وتتخطى ~/.profile — إذ لا يعمل ~/.profile إلا لجلسات تسجيل الدخول. تثبيت Node لكل مستخدم عبر مدير إصدارات مثل nvm يحقق الأمر نفسه؛ والهدف في الحالتين ألّا يحتاج npm install -g إلى sudo أبدًا. ما زال التثبيت عبر npm يعمل جيدًا، ويمكنك بدلًا منه استخدام سكربت التثبيت الأصلي من Anthropic، وهو الخيار الافتراضي الموثّق حاليًا. راجع وثائق التثبيت لدى Anthropic قبل أن تلصق الأوامر — فطرق التثبيت تتغير.
شغّل claude داخل مستودع لبدئه. في التشغيل الأول سيقودك خطوة بخطوة خلال المصادقة؛ والخادم بلا واجهة رسومية (headless) لا متصفح فيه، لذا تسلّمك عملية المصادقة رابط URL تفتحه على جهازك أنت ورمزًا تعود به إلى الطرفية. (وضع مفتاح API في متغيرات البيئة هو الطريق الآخر.) في الحالتين، صارت بيانات الاعتماد تلك الآن مخزّنة على الخادم — وهذا يقودنا إلى الجزء الذي يتخطاه الناس.
الحديث عن نطاق الضرر
الوكيل الذي يملك وصولًا إلى الـ shell هو نفسه shell. يستطيع قراءة كل ما يستطيع المستخدم الذي يعمل بحسابه قراءته، والدفع (push) إلى أي مكان يستطيع ذلك المستخدم الدفع إليه. هذا ليس نقدًا للأداة، بل هو تعريفها — ولهذا فإن الحساب الذي يعمل به أهم من أي إعداد منفرد.
- مستخدم مخصّص وغير مميّز الصلاحيات. لا عضوية في مجموعة
sudo، ولا مجلد منزل مشترك مع حسابك الشخصي. - لا بيانات اعتماد إنتاجية على الخادم. لا
~/.aws/credentialsيحمل مفاتيح الإنتاج، ولا ملف.envمنسوخ من بيئة الإنتاج، ولا كلمة مرور قاعدة بيانات لها صلاحية كتابة على أي شيء مهم. أعطِ الوكيل بيانات اعتماد خاصة ببيئة التجهيز (staging) أو للقراءة فقط. - رموز وصول (tokens) محدودة النطاق. رمز GitHub دقيق الصلاحيات (fine-grained) مقصور على مستودع واحد؛ ومفتاح نشر (deploy key) حين يكفي وصول القراءة.
يأتي Claude Code بخيار (flag) يتخطى مطالبات الأذونات لديه كليًا. على حاسوب محمول، وفي مشروع يمكن التخلص منه، القرار قرارك. أما على خادم يحمل رموز وصول، فهو يزيل آخر حاجز بين تعليمات أُسيء قراءتها وبين git push --force. ما الذي يغيّره هذا الخيار فعلًا، وكيف تحاصر وكيلًا يعمل به، من البيئة المعزولة (sandbox) المدمجة وصولًا إلى VPS يُستعمل ثم يُرمى، كل ذلك نتناوله في تشغيل Claude Code بأمان على خادم.
مفتاح النشر مقابل تمرير وكيل SSH
من المغري أن تتصل عبر ssh -A كي يستطيع git استخدام المفتاح الموجود على حاسوبك المحمول. افهم ما الذي يمنحه ذلك: تمرير الوكيل (agent forwarding) يكشف مقبس وكيل SSH المحلي لديك أمام العمليات التي تعمل بحساب ذلك المستخدم على الخادم. أي شيء يعمل بحساب agent — بما في ذلك الوكيل نفسه — يستطيع أن يطلب من مفتاحك التوقيع لصالح أي مضيف يمكنه الوصول إليه، ما دمت متصلًا. هذا أكثر بكثير من «دع git يسحب هذا المستودع الواحد».
بدلًا من ذلك، أنشئ مفتاحًا على الخادم، وسجّله مفتاح نشر خاصًا بمستودع بعينه (مع صلاحية الكتابة فقط إذا كان الوكيل يحتاج إلى الدفع)، واضبط هوية git حتى تكون الإيداعات (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"سير العمل مع tmux
ثبّته (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 [ وضع النسخ للتمرير إلى الخلف (q للخروج).
أوضاع الفشل
«جلستي اختفت.» يطبع tmux ls الرسالة no server running on /tmp/tmux-1000/default. هذا يعني في الغالب الأعم أن العملية لم تكن داخل tmux أصلًا — اتصلت عبر SSH وشغّلت claude مباشرة، فقتلها انقطاع الاتصال. لا شيء يمكن استرجاعه. والعادة التي تمنع ذلك: أن يكون tmux new -A -s <project> هو الأمر الأول بعد كل تسجيل دخول.
الجزء (pane) ينكمش إلى مربع صغير. يضبط tmux حجم الجلسة على أصغر عميل متصل بها، لذا فإن عميلًا قديمًا ما يزال متصلًا من جهاز آخر يقلّص مساحة العرض. افصل الآخرين قسرًا وأنت تتصل: tmux attach -d -t claude.
عملية بناء تطبع Killed. كلمة واحدة، بلا أثر مكدّس (stack trace). تأكد عبر sudo dmesg -T | grep -i -E 'out of memory|killed process' — قاتل نفاد الذاكرة في النواة (OOM killer) اختار أكبر عملية. من جهة Node قد ترى بدلًا من ذلك FATAL ERROR: Ineffective mark-compacts near heap limit Allocation failed - JavaScript heap out of memory. الإصلاحات بالترتيب: أضف swap (أعلاه)، وقيّد توازي الاختبارات والمترجم، وارفع سقف كومة الذاكرة (heap) في Node عبر NODE_OPTIONS=--max-old-space-size=...، أو كبّر خطة الـ VPS. قد يختار OOM killer أيضًا خادم tmux نفسه بدل عملية البناء، آخذًا جلستك معه؛ وإذا كان systemd-oomd يعمل، فبإمكانه قتل شريحة مستخدم (user slice) كاملة بالأثر نفسه.
npm error code EACCES / permission denied, mkdir '/usr/lib/node_modules/...'. تثبيت عام في بادئة مملوكة لـ root. استخدم بادئة ~/.npm-global أعلاه. وإذا كنت قد شغّلت sudo npm في وقت ما فقد ترى أيضًا Your cache folder contains root-owned files — أصلحها عبر sudo chown -R $(id -u):$(id -g) ~/.npm.
claude: command not found — لكن أحيانًا فقط. سطر تصدير PATH لديك يقع في ~/.bashrc تحت حارس "If not running interactively, don't do anything"، فتتخطاه جلسات الـ shell غير التفاعلية. انقل سطر التصدير فوق ذلك الحارس وأبقه في ~/.bashrc لا في ~/.profile: فقد يبدأ tmux جلسات shell ليست جلسات تسجيل دخول، وهذه تقرأ ~/.bashrc ولا تمسّ ~/.profile أبدًا.
ألوان مشوّهة بعد الاتصال بالجلسة. عدم تطابق في TERM — والسطر default-terminal أعلاه هو الحل.
الجلسات تختفي بعد إعادة التشغيل. ليست علّة: فخادم tmux عملية، وإعادة التشغيل تنهيها. تحقق من uptime.
ما الذي ينكسر مع التوسّع
مشاريع أكثر. جلسة tmux واحدة لكل مستودع، تحمل اسمه؛ وعندها يصبح tmux ls لوحة قيادتك. تجاهل انضباط التسمية وستحصل على جلسات باسم 0 و1 و2. المنافذ تتمدد بالطريقة نفسها — ستة مستودعات كلها تريد :3000 هي النقطة التي تتوقف عندها عن إسنادها يدويًا وتدع بروكسي Traefik عكسيًا يوجّه عدة تطبيقات تحت Docker Compose يتولى التوزيع حسب اسم المضيف.
أشخاص أكثر. مقابس tmux مفصولة لكل مستخدم، لذا يحصل كل واحد من مطوّرَين على الخادم نفسه على خادم tmux خاص به ولا يرى أحدهما جلسات الآخر. مشاركة جلسة واحدة عبر مقبس مشترك تعني أن الجميع يكتبون في الـ shell نفسه بهوية مستخدم Unix نفسها، مع ما يستتبعه ذلك من عواقب على التدقيق والأذونات. المستخدمون المنفصلون هم الجواب الممل والصحيح.
العمل دون إشراف. صُمم tmux للجلسات التفاعلية التي تتصل بها. أما المهام التي تعمل وفق جدول زمني ولا أحد يراقبها فمكانها وحدة systemd مع مؤقّت (timer)، حيث تحصل مجانًا على التسجيل في السجلّات وسياسة إعادة تشغيل والبقاء بعد الإقلاع. اللجوء إلى tmux لتشغيل مهمة على شاكلة cron علامة على أن المهمة تريد أن تكون خدمة.
ملاحظة أخيرة: اربط خوادم التطوير التي يشغّلها الوكيل بالعنوان 127.0.0.1 لا 0.0.0.0، وصِل إليها عبر نفق SSH (ssh -L 3000:127.0.0.1:3000 agent@your-server) بدل فتح منافذ في ufw. ومتى صرت تمرّر خمسة أو ستة منافذ، أو أراد هاتف وحاسوب محمول معًا الوصول إلى المعاينة نفسها، فضع أمامها بدلًا من ذلك شبكة WireGuard VPN مستضافة ذاتيًا على الـ VPS: ترتبط خوادم التطوير بواجهة شبكة خاصة، ويظل ufw يرفض كل شيء قادم من الواجهة العامة. جدار الحماية لا ينفع إلا إذا كففت عن فتح الثقوب فيه.
Claude Code ليس الخيار الوحيد: تشغيل وكيل ذكاء اصطناعي للبرمجة على VPS يوازن أيضًا بين Aider وGoose.
FAQ
هل يستمر Claude Code في العمل بعد انقطاع اتصال SSH؟
فقط إذا كنت قد بدأته داخل tmux. العملية التي تُطلق مباشرة من shell جلسة SSH هي عملية فرعية تابعة لذلك الـ shell وتموت مع الـ pty عند انقطاع الاتصال. داخل tmux ينتمي الـ shell إلى خادم tmux المنفصل، فيواصل الوكيل عمله في منتصف مهمته ويعيدك tmux attach إلى سجلّ التمرير نفسه. اجعل tmux new -A -s <project> أول أمر بعد كل تسجيل دخول وستزول المشكلة.
هل أثبّت الـ CLI باستخدام sudo npm install -g؟
لا. البادئة العامة المملوكة لـ root تسلّمك أخطاء EACCES في التثبيتات اللاحقة وملفات مملوكة لـ root في ذاكرة التخزين المؤقت لـ npm. اضبط بادئة npm على ~/.npm-global (أو استخدم مدير إصدارات مثل nvm)، وثبّت بحساب المستخدم غير المميّز agent، وصدّر ~/.npm-global/bin إلى PATH من ~/.bashrc، فوق حارس التفاعل. وإذا كنت قد شغّلت sudo npm مرة، فأصلح ذاكرة التخزين المؤقت عبر sudo chown -R $(id -u):$(id -g) ~/.npm.
هل تمرير الوكيل عبر ssh -A آمن على خادم يشغّل وكيلًا؟
إنه يمنح أكثر بكثير مما تحتاجه المهمة. التمرير يكشف مقبس وكيل SSH المحلي لديك أمام كل عملية تعمل بحساب ذلك المستخدم، فيستطيع أي شيء على الخادم أن يطلب من مفتاحك التوقيع لأي مضيف يمكنه الوصول إليه ما دمت متصلًا. أنشئ مفتاح ed25519 على الخادم وسجّله مفتاح نشر خاصًا بكل مستودع على حدة، مع صلاحية كتابة فقط إذا كان على الوكيل فعلًا أن يدفع تغييرات.
لماذا يطبع البناء عندي كلمة Killed فقط؟
كلمة واحدة بلا أثر مكدّس تعني قاتل نفاد الذاكرة في النواة (OOM killer). تأكد منه عبر sudo dmesg -T | grep -i -E 'out of memory|killed process'؛ ومن جهة Node قد ترى JavaScript heap out of memory بدلًا منها. تدرّج في الإصلاحات بالترتيب: أضف ملف swap، وقيّد توازي الاختبارات والمترجم، وارفع NODE_OPTIONS=--max-old-space-size=...، ثم كبّر خطة الـ VPS. وانتبه إلى أن OOM killer قد يختار خادم tmux بدل عملية البناء، آخذًا جلستك كلها معه.
tmux أم خدمة systemd؟
يناسب tmux الجلسات التفاعلية التي تتصل بها وتراقبها وتكتب فيها، وهذا هو حال جلسة الوكيل بالضبط. أما العمل الذي يجري وفق جدول زمني دون أن يراقبه أحد فمكانه وحدة systemd مع مؤقّت، حيث تأتي السجلّات وسياسة إعادة التشغيل والبقاء بعد الإقلاع مجانًا. إذا كنت تلجأ إلى tmux لتشغيل مهمة على شاكلة cron، فالمهمة تريد أن تكون خدمة.