- rc templates carry @USER@/@EMAIL@ placeholders; install.sh renders them - resolve_identity: an export already in ~/.bashrc or ~/.zshrc is reused silently, else IDENTITY_USER/IDENTITY_EMAIL, else a prompt, else login name + empty email - render_gitconfig generalised to render_identity_template (gitconfig unchanged) - vimrc reads g:_author/g:_email from $USER/$EMAIL instead of hardcoded values - README + CLAUDE.md
config
Personal dotfiles — vim + bash configuration and a one-shot installer.
Quick start
Install everything (clone + setup) with one command:
curl -fsSL https://git.bchanot.fr/bchanot/config/raw/branch/master/remote-install.sh | bash
(Runs a remote script through bash — see the Install section for what it does and the manual alternative.)
What's inside
| Path | Purpose |
|---|---|
install.sh |
Linux: installs apt packages + Docker + code-server + RDP (gnome-remote-desktop), backs up old config, deploys vim + bashrc (OS-detected), installs CLI scripts, pipx tools and a low-disk login warning; ends by offering three extras (/tmp on disk, SSH memory guard, the cloudpex NAS mount helper). macOS: same tooling through Homebrew (see macOS), deploys the tmux config, then prints what was not installed compared with Linux. |
tmux.conf |
tmux config (vi keys, mouse, status bar, tpm plugins: resurrect + continuum session restore, window-name). Deployed on both OSes to ~/.config/tmux/tmux.conf (tmux ≥ 3.1). |
cloudpex/ |
On-demand SMB mount of a NAS share (cloudpex command + its installer, offered [y/N] at the end of a Linux install). Site values (host, share, SMB user, mount point, SMB version) are prompted at install and stored in /etc/cloudpex.conf, never in the script. French README inside. |
etc/tmpfiles.d/tmp.conf |
Cleanup rules for a disk-backed /tmp (wiped at boot, 10-day purge). Deployed by the /tmp on disk offer. |
etc/systemd/ssh.service.d/override.conf |
ssh.service drop-in: sshd exempt from the OOM killer + memory reclaim protection. Deployed by the SSH memory guard offer. |
etc/default/earlyoom |
earlyoom arguments: spare sshd/systemd, kill node/java first. Deployed by the SSH memory guard offer. |
etc/fail2ban/jail.d/local.conf |
fail2ban sshd jail: journal backend, all-ports ban, 5 tries / 10 min / 1 h, private LAN never banned. Deployed on every Linux install. |
etc/apt/apt.conf.d/20auto-upgrades |
Enables unattended security upgrades (what dpkg-reconfigure writes). Deployed on every Linux install. |
etc/ssh/sshd_config.d/20-hardening.conf |
sshd limits that cannot lock you out: PermitRootLogin no, MaxAuthTries 3, LoginGraceTime 20. Deployed on every Linux install after sshd -t. |
vim/vimrc |
Vim config: pathogen, molokai, syntastic (C with -Wall -Werror -Wextra), NERDTree, 42-style canonical class generators (:ClassH, :ClassC). |
vim/autoload/ |
pathogen.vim plugin loader (committed). |
vim/colors/ |
molokai.vim colorscheme (committed). |
gitconfig |
Template of the user-scope ~/.gitconfig. @USER@ and @EMAIL@ are filled at install with the USER and EMAIL exported by the bashrc (git never expands $VARS itself). |
bash/bashrc-linux |
bashrc for desktop Linux (git-aware prompt + command timer). |
bash/bashrc-osx |
bashrc for macOS: bashrc-linux adapted (Homebrew on PATH, BSD ls -G, bash 5 clock for the timer, cc without systemd-run). |
zsh/zshrc-osx |
zshrc for macOS when zsh is chosen: oh-my-zsh + the same env, aliases and dtach menu as bashrc-osx. Loads ~/.zshrc.local for machine-specific lines. |
zsh/bchanot.zsh-theme |
oh-my-zsh theme reproducing the bash prompt: ✔ (12ms) user [ ~/dir ] [branch -*+] >. |
bin/dt |
dtach session manager for claude-in-dtach sessions. |
bin/dtach-router |
Dashboard to resume dtach sessions, shown at the start of every interactive shell (wired into ~/.bashrc by the installer). |
bin/claude-provider |
Switch Claude Code between Anthropic and OpenRouter. |
etc/profile.d/disk-usage-warning.sh |
Login-time warning (bold red) when / or /home cross 85% usage. Deployed to /etc/profile.d/ on Linux. |
Install
One-liner (clone + install)
curl -fsSL https://git.bchanot.fr/bchanot/config/raw/branch/master/remote-install.sh | bash
remote-install.sh ensures git is present, clones the repo to ~/config (or pulls if already there), then runs install.sh. Override with env vars: REPO_URL=... CLONE_DIR=... BRANCH=... curl ... | bash.
Piping a remote script into
bashruns unreviewed code over the network. Readremote-install.shfirst, or use the manual clone below.
Manual
git clone https://git.bchanot.fr/bchanot/config.git && cd config
./install.sh
No argument — the OS is auto-detected.
What it does:
- On Debian/Ubuntu, installs a set of CLI/dev packages via
apt-get(see below). On macOS, Homebrew does it instead: see macOS. - Sets up Docker's official apt repo (Ubuntu) and installs the engine + compose plugin — skipped if
dockeris already present. - Moves any existing
~/.vim,~/.vimrc,~/.bashrc,~/.Sublivimto~/Oldconfig. - Clones the
syntasticandnerdtreevim plugins into~/.vim/bundle/. - Copies the tracked vim files into
~/.vimand symlinks~/.vimrc. - Picks the bashrc by OS: macOS →
bashrc-osx(falls back tobashrc-linuxif missing), everything else →bashrc-linux. Renders it into~/.bashrcwith the identity asked at the very start: a name and an email for git commits and vim headers. Anexport USER=/export EMAIL=already present in~/.bashrcor~/.zshrcis reused without asking, so a re-run never prompts twice;IDENTITY_USER/IDENTITY_EMAILpreset them; with no terminal attached it falls back to the login name and an empty email. The values live only in the deployed files, never in the repo. Then rendersgitconfiginto~/.gitconfigwith the sameUSER/EMAIL. A different existing~/.gitconfigis saved as~/.gitconfig.backup-<date>; an identical one is left alone. It is the global level only: a repo's own.git/configstill overrides it.core.excludesfilepoints at~/.gitignore, ignored by git when the file does not exist. Then deploystmux.confto~/.config/tmux/tmux.conf(both OSes, tmux ≥ 3.1: Ubuntu 22.04+, brew), clones tpm and fetches the listed plugins headlessly; details under macOS step 6, the step is the same. On Linux,ycopies into tmux's buffer and the terminal clipboard through OSC 52; macOS usespbcopy/pbpaste. - Installs Python CLIs via
pipx(PyMuPDF→pymupdf,Markdown→markdown_py) — skipped ifpipxis absent. - Copies the
bin/scripts (dt,dtach-router,claude-provider) into~/.local/bin. The dtach session-resume menu ships in the deployed bashrc (both OSes), so every interactive shell offers it — including VS Code Remote-SSH terminals, which are non-login and never read~/.profile. The installer also strips any older dtach block left in~/.profileso a plain SSH login doesn't prompt twice. - On Linux, installs
etc/profile.d/disk-usage-warning.shto/etc/profile.d/(needssudo) so each login warns when/or/homecross 85% usage. - On Linux, installs code-server (VS Code in the browser) via its vendor script — skipped if already present — and enables the
code-server@<login>systemd service (login fromid -un: the bashrc overrides$USER). - On Linux, installs
ubuntu-desktop-minimal(GDM + GNOME Shell, ~1.5 GB): the RDP remote login below hands out a GNOME session, which a bare server install does not have. Then sets up RDP remote login viagnome-remote-desktop(Wayland-native): installs the daemon +openssl, generates a self-signed TLS cert once, and prompts interactively for shared "gate" credentials (skipped when no terminal is attached, or already set). Disablesxrdpif present; opens UFW port3389only when UFW is already active. Finally, whenlspcisees an NVIDIA GPU, runsubuntu-drivers installto put on the driver the distro recommends for the card (no version pinned; loads at the next reboot). Skipped on machines without an NVIDIA GPU. - On Linux, at the very end, offers (
[y/N]) to install thecloudpexNAS mount helper to/usr/local/binviacloudpex/install.sh, which prompts for the NAS host, share name, SMB user, mount point and SMB version and writes them to/etc/cloudpex.conf(root,0600; an existing config is shown and kept unless you sayn; skipped when no terminal is attached). Nothing is mounted, no password stored, seecloudpex/README.md. - On Linux, installs the security baseline, always, no prompt: fail2ban (+
nftables) withetc/fail2ban/jail.d/local.conf(sshd jail reading the journal, bans the offending IP on every port so the SSH port does not matter, 5 failures in 10 min → 1 h ban, loopback and private LAN ranges never banned); unattended-upgrades enabled throughetc/apt/apt.conf.d/20auto-upgrades; and the sshd drop-inetc/ssh/sshd_config.d/20-hardening.conf(PermitRootLogin no,MaxAuthTries 3,LoginGraceTime 20), checked withsshd -tand removed again if sshd rejects it, thenreload ssh. Authentication methods, port and user lists are left as they are. - On Linux, at the very end, offers (
[y/N], skipped when no terminal is attached) to move/tmpto disk: Ubuntu mounts/tmpas a RAM-backed tmpfs capped at 50% of RAM, which agent runs fill, halving the RAM and breaking every shell with "No space left on device". Accepting maskstmp.mountand installsetc/tmpfiles.d/tmp.conf(wipe at boot, 10-day purge). Effective at the next reboot. - On Linux, at the very end, offers to keep SSH reachable under memory pressure: installs the
ssh.servicedrop-in (OOMScoreAdjust=-1000,MemoryMin=256M) andearlyoomwithetc/default/earlyoom(kills the largest process,node/javafirst and neversshd, once free RAM and swap both drop under 10%). Restartingsshkeeps open sessions. Note:MemoryMinprotects the sshd daemon only; login sessions live inuser.slice, so no setting can reserve RAM for a future shell. earlyoom acting in time is the real protection.
Packages installed (apt)
- Build / VCS / C dev:
vim git git-lfs git-filter-repo gitleaks gcc make pkg-config dkms valgrind shellcheck gh git-delta(git-delta=delta, the pager set ingitconfig) - Net / security / transport:
curl gnupg ca-certificates apt-transport-https net-tools openssh-server cifs-utils lftp ftp - Shell tooling:
unzip tree tmux fzf dtach - Runtimes:
nodejs python3-pip pipx php-cli - Web stack (local WordPress/LAMP):
mariadb-server imagemagick php-mysql php-gd php-imagick php-mbstring php-xml php-intl php-curl(unversionedphp-*metapackages, so they follow the distro's PHP) - Media / doc CLI:
ffmpeg weasyprint poppler-utils qpdf webp libavif-bin - Docker:
docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin(via Docker's repo) - Desktop / GPU:
ubuntu-desktop-minimal(always, Linux) + the distro-recommended NVIDIA driver viaubuntu-drivers install(only when an NVIDIA GPU is detected) - Remote access:
gnome-remote-desktop openssl(apt) +code-server(via its vendor install script, not apt) — RDP remote login + browser VS Code - pipx:
PyMuPDF(pymupdf),Markdown(markdown_py) - Security baseline (Linux, always):
fail2ban nftables unattended-upgrades - Optional (end-of-install offer, Linux):
earlyoom
The script is re-runnable: each run re-backs up to ~/Oldconfig (overwriting the previous backup), re-clones plugins, skips Docker if already installed, and re-deploys the bin/ scripts.
Note: the Docker repo step assumes Ubuntu.
macOS
The same ./install.sh detects macOS and replaces apt-get with Homebrew. It first asks which login shell you want, bash or zsh ([bash] by default; answer in advance with MACOS_SHELL=zsh ./install.sh, and with no terminal attached it picks bash):
- Installs Homebrew with its official script when
brewis missing (this also pulls the Xcode Command Line Tools: clang, make, git), thenbrew update+brew upgrade. - Installs the apt list mapped to formulae:
vim git git-lfs git-filter-repo gitleaks pkgconf shellcheck gh git-delta curl gnupg lftp inetutils unzip tree tmux fzf dtach node python pipx php mariadb imagemagick ffmpeg weasyprint poppler qpdf webp libavif bash. Brew'sphpalready ships gd, mbstring, xml, intl, curl and mysql. - Docker:
colima(the Linux VM) +docker docker-compose docker-buildx. Writes~/.docker/config.jsonwithcliPluginsExtraDirssodocker composeworks, only when that file does not exist yet (otherwise prints the line to add). - Starts
colima,code-serverandmariadbasbrew services(thesystemctl enable --nowequivalent), skipping any already started. - Deploys
bashrc-osx, then appends one line to~/.bash_profilethat sources~/.bashrc: macOS terminals open login shells, which never read~/.bashrcon their own. Done for both choices, sobashstays usable. - Deploys
tmux.confto~/.config/tmux/tmux.conf(same step on Linux), clones tpm into~/.config/tmux/plugins/tpmand fetches the listed plugins headlessly (resurrect + continuum restore your sessions, window-name renames windows after what runs in them). A~/.tmux.confwould be read instead, so one found is moved to~/.tmux.conf.backup-<date>; a differing~/.config/tmux/tmux.confbecomestmux.conf.backup-<date>.libtmux(needed by window-name) goes in the python user site; brew's python blocks that by default, so the install retries with--break-system-packages. Both plugin steps only warn on failure:prefix+Iinside tmux fetches the plugins. Prefix isC-a. - bash chosen: makes brew's bash 5 the login shell (adds it to
/etc/shellswithsudo, thenchsh, which asks for your password). macOS ships bash 3.2, too old for the bashrc. zsh chosen: installs oh-my-zsh with its official script (unattended, skipped if~/.oh-my-zshexists), renderszsh/zshrc-osxinto~/.zshrc(same identity as the bashrc) and deploys thebchanottheme to~/.oh-my-zsh/custom/themes/, then makes/bin/zshthe login shell. An existing~/.zshrcthat differs from the repo's is saved as~/.zshrc.backup-<date>(outside~/Oldconfig, which every run wipes). Move your machine-specific lines (nvm, bun, tokens) into~/.zshrc.local: the deployed zshrc loads it. - Ends with the list of what the Linux install has and this one does not:
gcc(Apple clang answers togcc),valgrind,dkms,net-tools,openssh-serverand the RDP desktop (both built into macOS, switched on in System Settings > Sharing),cifs-utils,php-imagick, the NVIDIA driver, the disk-usage warning,cloudpex, the security baseline and the two end-of-install offers.
CLI scripts (bin/)
Deployed to ~/.local/bin (the deployed bashrc adds this dir to PATH):
dt— manage claude-in-dtach sessions (dt ls|at|kill). Needsdtach+fzf.dtach-router— session dashboard shown at shell startup. It ships in the deployed bashrc and is sourced (not executed) in every interactive shell, so it also fires in VS Code Remote-SSH terminals (non-login shells that skip~/.profile). Silent no-op when no session exists. Create a session withcc [name], re-open the menu anytime withd(both aliases from the bashrc). Needsdt,dtach,fzf.claude-provider— switch Claude Code between Anthropic and OpenRouter. OpenRouter mode reads the key from$OPENROUTER_API_KEY(never hardcoded). Export it from a private, untracked file, e.g.~/.bashrc.local:export OPENROUTER_API_KEY="<your-openrouter-key>"
Requirements
bash,git- Debian/Ubuntu
apt-get, or macOS (Homebrew is installed if missing) - A
bashlogin shell on Linux (zsh users switch to bash for these prompts to apply). On macOS the installer sets bash or zsh, your choice
License
GPL-3.0-or-later — see LICENSE.
Copyright (C) 2026 Bastien Chanot.