← 返回首页

把本机 zsh + oh-my-zsh + atuin + Claude Code 一键搬到一台空白远端 Ubuntu

2026-05-26 · LinuxzshatuinClaude CodeUbuntu

环境: 源机 Ubuntu 26.04(zsh + oh-my-zsh + powerlevel10k + atuin + anaconda3),目标机 Ubuntu 24.04 全新装(只有 bash + anaconda3)。两台都在内网,目标机 SSH 端口 50022,有一个走代理的环境 192.168.2.134:15236

新到手一台机器,只有 bash。想要本机这一整套 zsh + p10k + atuin + Claude Code 全搬过去,把日常 SSH 进去操作的体验做齐。这件事看着只是几条 rsync + apt install,但中间有四个坑值得记一下。

第一步:解决"sudo 要密码"

ssh 进去能用,但 sudo apt install 立刻要密码,后续脚本就跑不动。在那边手动执行一次(用 visudo 安全写到 sudoers.d 而不是改主文件):

echo "zj ALL=(ALL) NOPASSWD:ALL" | sudo EDITOR='tee' visudo -f /etc/sudoers.d/zj-nopasswd
sudo chmod 440 /etc/sudoers.d/zj-nopasswd

验证:

sudo -k && sudo -n true && echo OK

回滚就 sudo rm /etc/sudoers.d/zj-nopasswd。注意这会让任何拿到该用户 shell 的人无密码取得 root,只在你信任的机器上做。

第二步:装 zsh,把本机配置 rsync 过去

目标机先装 zsh / fzf:

ssh -p 50022 zj@target 'sudo -n apt install -y zsh fzf'

把本机的 .oh-my-zsh.atuin.p10k.zsh 整个 rsync 过去:

rsync -az -e 'ssh -p 50022' ~/.oh-my-zsh/   zj@target:~/.oh-my-zsh/
rsync -az -e 'ssh -p 50022' ~/.atuin/       zj@target:~/.atuin/
rsync -az -e 'ssh -p 50022' ~/.config/atuin/ zj@target:~/.config/atuin/
scp -P 50022 ~/.p10k.zsh zj@target:~/.p10k.zsh

写一份改造版 .zshrc(直接拷本机的会带上一些环境差异),改两处:

scp 过去:

scp -P 50022 .zshrc.remote zj@target:~/.zshrc
ssh -p 50022 zj@target 'sudo -n chsh -s /usr/bin/zsh zj'

第三步(坑 1):别把 fail2ban 招出来

我上一步是用脚本同时跑的 rsync + scp + 多次 ssh 烟测——结果下一次 ssh 直接 Connection refused:

ssh: connect to host target port 50022: Connection refused

但 ping 通,主机活着。典型症状:Ubuntu Server 默认装了 fail2ban 且默认开 sshd jail,十几次 SSH 会话在短时间内涌上去触发了封禁规则。先在那边那个还活着的 bash 会话(没掉就万幸,掉了只能 IPMI / 物理终端)里诊断:

sudo systemctl status fail2ban sshguard
sudo fail2ban-client status sshd
sudo fail2ban-client set sshd unbanip <你的源 IP>

如果是 nftables / iptables 直接 REJECT,也要顺便看一眼:

sudo nft list ruleset | grep -E "50022|reject"
sudo iptables -L -n -v | grep -E "50022|REJECT|DROP"

经验:批量自动化连同一台机器时,合并成一个 SSH 会话里跑,别开十几条短连接。

第四步(坑 2):atuin 导入 bash 历史,$ATUIN_SESSION 是假错

把目标机的 .bash_history 一次性灌进 atuin:

HISTFILE=~/.bash_history ~/.atuin/bin/atuin import bash

会看到一段大象 ascii + Import complete!。但立刻跑 atuin statsatuin search 会报错:

Error: Failed to find $ATUIN_SESSION in the environment.
Check that you have correctly set up your shell.

第一次看到会以为是装坏了。其实没坏——$ATUIN_SESSION 是 atuin shell 集成在交互 shell 启动时注入的,我用 ssh host 'atuin stats' 这种非交互模式跑,自然没有这个变量。导入本身已经成功,换交互 shell 里查就正常:

ssh -t zj@target          # 注意 -t 强制 tty,进交互 zsh
atuin stats               # 这次正常

也可以用 zsh -ic 'atuin stats',但输出里会带一段 gitstatus 启动告警(非交互模式 zle 加载失败,无影响)。

第五步(坑 3):conda env export 默认不导出 pip 段

本机有个 p311 环境,里面 70 多个包是用 pip 装的(包括一个本地路径的 torch wheel)。直觉操作:

conda env export -n p311 --no-builds > p311.yml
# 拷到目标机
conda env create -f p311.yml -n p311

跑完一看——目标机 p311/ 里只有 python + pip 这种壳级别的包,所有 pip 装的包全丢了

原因:conda env export 只列 conda 注册的包。pip 装的包除非满足两个条件之一才会出现在 yaml 的 - pip: 段里:

更稳的做法:conda 包用 conda env export,pip 包另开一份 pip freeze

# 在源机
conda env export -n p311 --no-builds > p311_conda.yml
/home/zj/anaconda3/envs/p311/bin/pip freeze > p311_pip.txt

pip freeze 出来的列表里要小心两类条目:

目标机:

# 先建空环境
ssh -p 50022 zj@target '~/anaconda3/bin/conda create -n p311 python=3.11 pip -y'
# 装 conda 包
scp -P 50022 p311_conda.yml zj@target:/tmp/
ssh -p 50022 zj@target '~/anaconda3/bin/conda env update -n p311 -f /tmp/p311_conda.yml'
# 再装 pip 包
scp -P 50022 p311_pip.txt zj@target:/tmp/
ssh -p 50022 zj@target '
  export HTTPS_PROXY=http://192.168.2.134:15236
  ~/anaconda3/envs/p311/bin/pip install -r /tmp/p311_pip.txt
'

另一个细节:conda 走的是 USTC 等国内镜像,不需要走代理(走代理反而可能 SSL 握手坏掉);pip 走 PyPI 需要代理。两条命令的代理设置要分开,别一股脑全套上 https_proxy

第六步:进目录自动激活 conda env

要做"cd ~/projects/foo 自动 conda activate p311",最干净的是 direnv + .envrc:

sudo apt install direnv
# ~/.zshrc 末尾追加
eval "$(direnv hook zsh)"

每个项目目录放一个 .envrc:

layout anaconda p311
# 或更通用:
# eval "$(conda shell.bash hook)" && conda activate p311

第一次进目录被拦下,执行一次 direnv allow(防止恶意 repo 自带 .envrc 自动跑代码),之后自动激活,出目录自动 deactivate。带缓存所以基本无延迟。

替代品对比:

方案优点缺点
direnv通用、快、安全门槛direnv allow 一次
zsh chpwd 钩子零依赖自己写规则、容易出错
conda-auto-env 插件跟 oh-my-zsh 集成只认 environment.yml,不灵活
autoenv简单维护停滞、安全模型差

第七步(坑 4):装 Claude Code,native 路径不在 PATH

官方一键脚本走 native 安装(不需要 Node):

ssh -p 50022 zj@target '
  export HTTPS_PROXY=http://192.168.2.134:15236
  curl -fsSL https://claude.ai/install.sh | bash
'

会装到 ~/.local/bin/claude。脚本结尾有个 setup 警告:

⚠ Setup notes:
  ● Native installation exists but ~/.local/bin is not in your PATH. Run:
    echo 'export PATH="$HOME/.local/bin:$PATH"' >> ~/.zshrc && source ~/.zshrc

照着做就行。验证:

ssh -p 50022 zj@target 'zsh -ic "which claude && claude --version"'

应输出 ~/.local/bin/claude2.1.119 (Claude Code) 之类。第一次 claude 会引导浏览器登录。

小结

整套搬迁的关键不是 rsync 本身,而是踩准这四个坑:fail2ban 别招出来、atuin 在非交互 shell 报错是假象、conda env export 漏 pip 段、Claude Code native 装完要补 PATH。各自都不难,但攒在一起足够卡你半小时。