An interactive provisioning script for deploying a fully configured Linux environment. Optimized for Arch Linux and Debian/Kali (including WSL). Centralizes config files and scripts across multiple machines using GNU Stow and Git.
Remote execution (fresh machine):
wget -qO shellSetup.sh https://shell.ivantsov.tech && bash shellSetup.shcurl -fsSL https://shell.ivantsov.tech | bashRe-running on an existing machine:
cd ~/linuxploitacious && git pull && bash shellSetup.shThe script is fully idempotent. It can be re-run at any time without conflicts. Existing configs are backed up automatically and the repository is always deployed as the source of truth.
All components are designed to be safely re-run:
| Component | Safety Mechanism |
|---|---|
| Package install | --needed flag (Arch) won't reinstall existing packages |
| Oh My Zsh | Skips if ~/.oh-my-zsh already exists |
| Oh My Posh | Skips if binary already installed |
| TPM | Skips if ~/.tmux/plugins/tpm exists |
| Stow | Removes old symlinks first, then deploys fresh |
| NVM | Skips if ~/.nvm already exists |
| Node.js | nvm install --lts is safe to re-run |
| AI tool symlinks | Removes and recreates symlinks (no duplicates) |
| SSH keys | Skips if key already exists |
No data loss - sessions, configs, and credentials are preserved on re-run.
The script uses a two-stage architecture:
- Remote stage - When piped from a URL (
curl | bash), it detects there's no.gitdirectory, installs Git, clones the repo to~/linuxploitacious, then usesexec ./shellSetup.shto hand off to the local copy. - Local stage - Presents an interactive menu (via
whiptail) and executes selected components.
| Option | Description | Default |
|---|---|---|
| BASE | OS updates, core packages (zsh, stow, tmux, fzf, btop, fastfetch, etc.) | ON |
| NODE | Node.js via NVM and pnpm (required by openclaw tooling) | ON |
| PYTHON | Python via pyenv + pip packages | ON |
| SHELL | Zsh, Oh My Zsh (with autosuggestions, syntax-highlighting, completions, fzf-tab), Oh My Posh, TPM | ON |
| STOW | Deploy all repo configs to $HOME via GNU Stow |
ON |
| TMUX | Tmux persistence: SSH auto-attach + tmux-main.service systemd unit + linger |
ON |
| DOCKER | Docker Engine (docker-ce + buildx + compose plugin), adds user to docker group |
OFF |
| BRAVE | Brave Browser | OFF |
| ROOT | Replicate user profile to root (configs, NVM, OMZ, OMP, TPM) | OFF (hidden when running as root) |
| NOPASS | Enable passwordless sudo for current user (drops /etc/sudoers.d/90-<user>-nopasswd, visudo-validated) |
OFF (hidden when running as root) |
| SWAP | Create swapfile sized to match RAM (/swapfile, vm.swappiness=10) |
ON (hidden when swap already exists) |
| SSHKEY | Generate GitHub SSH key, configure SSH, copy to root | OFF |
| COWORK | Clone COWORK repo + Multi-Agent Coordination (requires SSHKEY) | OFF |
Always runs (no menu toggle): Claude Code and OpenCode install via their official vendor scripts (curl -fsSL https://claude.ai/install.sh | bash and curl -fsSL https://opencode.ai/install | bash), dropping native binaries into ~/.local/bin/claude and ~/.opencode/bin/opencode. Legacy pnpm/npm-global installations of these tools (and Gemini) are detected and removed on every run to prevent shims from shadowing the native binaries.
Supply chain note: The two installers are fetched unpinned at setup time. Acceptable for a personal dotfiles repo; pin to a version (
bash -s <version>for Claude) or checksum-verify if you plan to run this on machines you don't own.
Claude Code configuration is deployed in two layers:
STOW (global config): The claude/ directory deploys ~/.claude/CLAUDE.md (behavioral rules, conversational compression) and ~/.claude/settings.json (model, effort level, permissions) using absolute symlinks instead of stow. This is necessary because stow creates relative symlinks that break when chained through the ROOT profile's ~/.claude → /home/user/.claude symlink. These config files apply to every Claude Code session regardless of project directory.
Plugins: After Claude Code installs (via the always-on vendor installer), the script registers the caveman plugin — an ultra-compressed communication mode that reduces token usage while keeping full technical accuracy. It activates automatically via SessionStart hooks.
ROOT sharing: The ROOT option symlinks ~/.claude/ from the user account to /root, so both users share the same config, sessions, and credentials. The claude/ stow package is intentionally excluded from ROOT's stow deployment to avoid conflicting with this symlink.
Personalizing Claude with a context directory: The global CLAUDE.md includes a "Context Awareness" section that checks for ~/COWORK/CONTEXT/ at session start. If you fork this repo, replace that path with your own. The idea: keep a directory somewhere on your machine with markdown files that describe who you are, how you communicate, and how you want Claude to behave (about-me.md, brand-voice.md, working-preferences.md, or whatever fits). The global CLAUDE.md points Claude there so every session starts with that context, even when you're working in an unrelated project. You don't need a full COWORK setup -- any directory with a few context files works. Project-level CLAUDE.md files then layer on top for project-specific instructions.
The ROOT option replicates your user profile to the root account:
- Installs missing packages (btop, tmux, fzf, zsh)
- Changes root's shell to zsh
- Installs Oh My Zsh, Oh My Posh, and TPM for root
- Deploys configs via stow to
/root(symlinks to this repo) - Installs NVM, Node.js LTS, and pnpm for root
- Installs Claude Code and OpenCode for root via their official vendor installers (native binaries, no npm)
- Shares AI tool data directories with root (Claude Code, OpenCode)
AI Tool Data Sharing:
The following directories are symlinked from your user to /root, allowing seamless session continuity:
~/.claude/- Claude Code sessions and credentials~/.claude.json- Claude Code config~/.local/share/opencode/- OpenCode database and auth
After running: Use sudo -i to access root's configured environment.
The SSHKEY option sets up SSH authentication for GitHub:
- Generates ed25519 SSH key at
~/.ssh/id_ed25519(if not exists) - Uses email from
git config --global user.emailfor the key comment - Configures
~/.ssh/configfor GitHub - Switches git remote from HTTPS to SSH
- Copies key pair to
/root/.ssh/for root access
After running: Upload the displayed public key to https://github.com/settings/keys
GNU Stow is a stateless symlink manager. It has no daemon, no database. It reads a "package" directory and creates symlinks that mirror its structure into a target directory ($HOME).
Stow ignores the package folder name itself. The contents of the package folder map directly to $HOME:
stow -t ~ zsh # zsh/.zshrc -> ~/.zshrc
stow -t ~ tmux # tmux/.tmux.conf -> ~/.tmux.conf
stow -t ~ btop # btop/.config/btop/ -> ~/.config/btop/
When deploying, the script runs four phases per package to guarantee a clean deployment:
| Phase | Action | Purpose |
|---|---|---|
| 1. Unstow | stow -D removes previous symlinks |
Clean slate from prior runs |
| 2. Clean | Remove stale/broken symlinks at targets | Handle moved or renamed files |
| 3. Backup | Move real files to <file>.backup_<timestamp> |
Preserve local configs safely |
| 4. Stow | stow -v creates fresh symlinks |
Deploy repo as source of truth |
This means: the repository always wins. Any local file that conflicts gets timestamped backups (never overwritten), and the symlink is recreated pointing to the repo.
Rule: The repo root is strictly for management scripts and documentation. All deployable files must exist inside a designated package folder.
~/linuxploitacious/
├── README.md # This documentation (not stowed)
├── shellSetup.sh # Bootstrap script (not stowed)
├── bash/ # Package: Bash config
│ └── .bashrc # -> ~/.bashrc
├── zsh/ # Package: Zsh config
│ └── .zshrc # -> ~/.zshrc
├── tmux/ # Package: Tmux config
│ └── .tmux.conf # -> ~/.tmux.conf
├── btop/ # Package: Btop config
│ └── .config/btop/
│ └── btop.conf # -> ~/.config/btop/btop.conf
├── fastfetch/ # Package: Fastfetch config
│ └── .config/fastfetch/
│ └── config.jsonc # -> ~/.config/fastfetch/config.jsonc
├── omp/ # Package: Oh My Posh themes
│ └── .config/ohmyposh/
│ ├── kali.json # -> ~/.config/ohmyposh/kali.json
│ └── zen.toml # -> ~/.config/ohmyposh/zen.toml
├── claude/ # Package: Claude Code global config
│ └── .claude/
│ ├── CLAUDE.md # -> ~/.claude/CLAUDE.md
│ └── settings.json # -> ~/.claude/settings.json
├── rustscan/ # Package: RustScan config
│ └── .rustscan.toml # -> ~/.rustscan.toml
├── scripts/ # Package: Utility scripts
│ └── .local/bin/
│ ├── start-kex # -> ~/.local/bin/start-kex
│ ├── usb-attach # -> ~/.local/bin/usb-attach
│ ├── pbcopy # -> ~/.local/bin/pbcopy
│ ├── pbpaste # -> ~/.local/bin/pbpaste
│ └── launch_nordvpn # -> ~/.local/bin/launch_nordvpn
└── dockerHost/ # NOT a stow package (docker infra, unrelated)
├── docker-compose.yml
└── dockerhost.md
These aliases are defined in both .bashrc and .zshrc and available in either shell.
| Alias | Command | Description |
|---|---|---|
c |
clear |
Clear the terminal |
x |
exit |
Exit the shell |
e |
code -n ~/ ~/.zshrc ... |
Open home dir and shell config in VS Code |
r |
source ~/.zshrc / source ~/.bashrc |
Reload shell configuration |
h |
history -10 |
Show last 10 history entries |
hc |
history -c |
Clear shell history |
hg |
history | grep |
Search history (e.g., hg docker) |
ag |
alias | grep |
Search aliases (e.g., ag rust) |
| Alias | Command | Description |
|---|---|---|
ls |
ls -lFh --color=auto --time-style=long-iso |
Detailed listing, hides dotfiles |
lsa |
ls -alFh --color=auto --time-style=long-iso |
Detailed listing including dotfiles |
la |
(same as lsa) |
Alias for lsa |
ll |
(same as lsa) |
Alias for lsa (muscle-memory shortcut) |
cd.. |
cd .. |
Go up one directory (typo-friendly) |
cd... |
cd .. && cd .. |
Go up two directories |
vsc |
cd /mnt/c/users/Alex/VSCODE |
Jump to VS Code workspace (WSL) |
| Alias | Command | Description |
|---|---|---|
sapu |
sudo apt-get update |
Quick apt update |
| Alias | Command | Description |
|---|---|---|
myip |
curl -s http://ipecho.net/plain; echo |
Show public IP address |
connectnord |
sudo ~/.local/bin/launch_nordvpn |
Launch NordVPN (OpenVPN wrapper) |
rustscan |
sudo docker run ... rustscan/rustscan:2.1.1 |
Run RustScan via Docker (full port scan, hands off to nmap) |
| Alias | Command | Description |
|---|---|---|
gcu |
git config user.name "..." && git config user.email "..." |
Set local git identity |
distro |
cat /etc/*-release |
Show distro information |
| Script | Description |
|---|---|
start-kex |
Manages Kali Win-KeX sessions (ESM/VNC/RDP) for WSL |
usb-attach |
Forwards USB devices from Windows to WSL via usbip |
pbcopy / pbpaste |
macOS-style clipboard commands using xclip |
launch_nordvpn |
Self-provisioning NordVPN OpenVPN wrapper with random server selection |
| Setting | Value |
|---|---|
| Prefix | Ctrl+a (screen-default — avoids the Windows IME Ctrl+a collision in Termius/PuTTY) |
| Window/pane indexing | Starts at 1 (not 0) |
| Copy mode | Vi keybindings |
| Mouse | Enabled |
| History limit | 50 000 lines |
| Theme | Catppuccin Mocha (via TPM) |
| Pane splits | Open in current working directory |
| Reload bind | PFX r runs source-file ~/.tmux.conf |
First run: TPM auto-clones itself on first launch and install_plugins runs once. After that, plugins are managed via PFX I (install) and PFX U (update).
| Plugin | Purpose |
|---|---|
tmux-plugins/tpm |
Plugin manager |
tmux-plugins/tmux-sensible |
Sane defaults (escape-time, history, etc.) |
dreamsofcode-io/catppuccin-tmux |
Theme (mocha flavour) |
tmux-plugins/tmux-resurrect |
Save/restore sessions, windows, panes, running programs, and pane contents. Manual: PFX Ctrl+s save, PFX Ctrl+r restore |
tmux-plugins/tmux-continuum |
Auto-saves resurrect every 15 min; auto-restores on tmux server start (pairs with tmux-main.service) |
tmux-plugins/tmux-yank |
y in copy mode + mouse-drag → system clipboard via OSC52 (works through Termius → Windows) |
tmux-plugins/tmux-prefix-highlight |
Status-bar indicator that lights up when prefix is active or in copy/sync mode |
tmux-plugins/tmux-cpu |
CPU / RAM widgets for status bar |
tmux-plugins/tmux-online-status |
Green/red dot showing internet reachability |
PFX-indicator │ CPU% │ RAM% │ online-dot │ Public IP │ YYYY-MM-DD HH:MM
Public IP is fetched via the pubip script (~/.local/bin/pubip) which caches to ~/.cache/pubip for 5 minutes — keeps the status bar fast and stops every refresh from hitting api.ipify.org.
The TMUX menu option wires three things so your work survives SSH disconnects, reboots, and laptop closes:
- Auto-attach on SSH login —
.zshrcand.bashrcship with a snippet that, on SSH-originated shells outside an existing tmux, runstmux attach -t main 2>/dev/null || tmux new -s main. SSH in → land directly in themainsession every time. tmux-main.service— a~/.config/systemd/user/tmux-main.serviceunit that startstmux new-session -d -s mainat boot. Survives reboots.loginctl enable-linger <user>— user services run without an active login session, so step 2 actually triggers at boot rather than at first login.
To detach from a session and keep it running: Ctrl+a d. Do not exit — that kills the pane (and if it's the only pane, the window; if the only window, the session).
The prefix on this setup is Ctrl+a (written as PFX below). All commands except shell ones are typed after hitting the prefix.
Shell (outside tmux)
| Command | Purpose |
|---|---|
tmux |
Start unnamed session |
tmux new -s <name> |
Start named session |
tmux ls |
List sessions |
tmux a |
Attach to most recent session |
tmux a -t <name> |
Attach to specific session |
tmux kill-session -t <name> |
Kill one session |
tmux kill-server |
Kill all sessions (nukes tmux server) |
Sessions (inside tmux)
| Keys | Purpose |
|---|---|
PFX d |
Detach (keeps session alive) |
PFX s |
Interactive session picker |
PFX $ |
Rename current session |
PFX ( / PFX ) |
Previous / next session |
Windows (tabs)
| Keys | Purpose |
|---|---|
PFX c |
New window |
PFX , |
Rename window |
PFX & |
Kill window (confirms) |
PFX n / PFX p |
Next / previous window |
PFX 1 … PFX 9 |
Jump to window by number |
PFX w |
Interactive window picker |
Panes (splits)
| Keys | Purpose |
|---|---|
PFX " |
Split horizontally (top/bottom) — opens in $PWD |
PFX % |
Split vertically (left/right) — opens in $PWD |
PFX <arrow> |
Move focus between panes |
PFX x |
Kill current pane (confirms) |
PFX z |
Toggle pane zoom (fullscreen current pane) |
PFX { / PFX } |
Swap pane with previous / next |
PFX SPACE |
Cycle pane layouts |
PFX q |
Show pane numbers (press number to jump) |
Copy mode (vi keys enabled)
| Keys | Purpose |
|---|---|
PFX [ |
Enter copy mode |
v |
Start selection (vi-style) |
y |
Yank selection to tmux buffer |
q |
Exit copy mode |
PFX ] |
Paste most recent buffer |
PFX = |
Show buffer list |
| Mouse drag | Select + auto-copy (mouse is on) |
Misc
| Keys | Purpose |
|---|---|
PFX ? |
Show all keybindings |
PFX : |
Command prompt (:source-file ~/.tmux.conf, etc.) |
PFX r (if bound) |
Reload .tmux.conf — not bound by default; use :source ~/.tmux.conf |
PFX I |
Install TPM plugins (capital i) |
PFX U |
Update TPM plugins |
ssh vm # SSH in → drops you into 'main' session automatically
# ... work ...
Ctrl+a d # detach, close laptop, walk away
ssh vm # reconnect later → same session, same stateIf main ever gets cluttered, spin up project-scoped sessions:
tmux new -s coworkdev # for COWORK work
tmux new -s claude # for a long Claude session
PFX s # inside tmux: switch between themTo survive flaky networks, install Mosh on both ends and replace ssh vm with mosh vm -- tmux a -t main.
This guide explains how to use Snapper on your Arch Linux system to create, manage, and restore system snapshots. Your system is configured to take automatic snapshots of the root filesystem (/).
- Tool:
snapper(Btrfs snapshot manager). - Target: Root filesystem (
/). - Automatic Retention Policy:
- Hourly: Keeps the last 10 hourly snapshots.
- Daily: Keeps the last 7 daily snapshots.
- Weekly/Monthly/Yearly: Disabled (0).
- Package Manager Integration:
- snap-pac: Installed. Automatically creates a "pre" snapshot before any pacman transaction and a "post" snapshot after.
To see all current snapshots, including their type (single, pre, post) and description:
sudo snapper -c root listBefore making risky changes (e.g., editing system configs, installing experimental software), create a manual checkpoint:
sudo snapper -c root create -d "Description of the checkpoint"-c root: Specifies the config (root filesystem).-d "...": Adds a description to the snapshot.
To manually delete a specific snapshot (replace NUMBER with the ID from list):
sudo snapper -c root delete NUMBERTo see what files changed between two snapshots (e.g., between snapshot 10 and 11):
sudo snapper -c root status 10..11To see the actual content differences (diff) of a specific file:
sudo snapper -c root diff 10..11 /path/to/fileSince your system uses a standard Btrfs layout with specific subvolumes (@, @home, @snapshots), the safest way to restore the entire system is via a live environment.
Restoring the root filesystem will revert system files (/etc, /usr, /var) to a previous state. Your home directory (/home) is on a separate subvolume (@home) and will not be touched, so your personal files are safe.
-
Boot Live ISO: Insert your Arch Linux USB installation media and boot into it.
-
Mount Root Partition: Identify your root partition (likely
/dev/nvme0n1p4based on your setup):mount /dev/nvme0n1p4 /mnt
-
Locate Snapshot: Snapshots are stored in
.snapshots/. List them to find the one you want (check the timestamp):ls -l /mnt/@snapshots/*/snapshotNote the number of the snapshot directory (e.g.,
/mnt/@snapshots/55/snapshot). -
Backup Current State (Optional but Recommended): Move the current broken system subvolume to a backup name:
mv /mnt/@ /mnt/@_broken_$(date +%Y%m%d) -
Restore the Snapshot: Create a read-write snapshot of the desired backup into the root position (
@):btrfs subvolume snapshot /mnt/@snapshots/NUMBER/snapshot /mnt/@
(Replace
NUMBERwith the snapshot ID you chose). -
Reboot: Unmount and reboot into your restored system:
umount /mnt reboot
Once you have successfully booted and verified the system is working, you can delete the broken backup subvolume to free up space:
sudo btrfs subvolume delete /mnt/@_broken_YYYYMMDDCause: Oh My Posh can't find ~/.config/ohmyposh/kali.json.
Fix: Re-run the STOW deployment:
cd ~/linuxploitacious && bash shellSetup.sh
# Select STOW from the menuOr deploy manually:
cd ~/linuxploitacious && stow -R -t ~ ompCause: A real file exists where stow wants to create a symlink.
Fix: The setup script handles this automatically by backing up conflicting files. If running stow manually:
# Back up the conflicting file, then restow
mv ~/.zshrc ~/.zshrc.backup
cd ~/linuxploitacious && stow -t ~ zshCause: Files were renamed or moved in the repo, but old symlinks still exist in $HOME.
Fix: Unstow the old package, then stow fresh:
cd ~/linuxploitacious
stow -D -t ~ <package> # Remove old symlinks
stow -t ~ <package> # Create new onesOr simply re-run shellSetup.sh -- it does this automatically.
Check that configs are properly symlinked:
ls -la ~/.zshrc ~/.bashrc ~/.tmux.conf
# Should show -> linuxploitacious/...
ls -la ~/.config/ohmyposh/ ~/.config/fastfetch/ ~/.config/btop/
# Should show -> ../linuxploitacious/... or actual symlinked filesAfter editing configs in the repo, reload your shell:
r # Uses the 'r' alias to re-source your shell config
# OR
source ~/.zshrc
source ~/.bashrcBoth commands are wrapped in command -v guards. If they don't appear:
- Verify they're installed:
which oh-my-posh/which fastfetch - If missing, re-run
shellSetup.shwith BASE selected - Ensure
~/.local/binis in your PATH:echo $PATH | tr ':' '\n' | grep local
cd ~/linuxploitacious
# Deploy a single package
stow -t ~ zsh
# Restow (unstow + stow, good for updates)
stow -R -t ~ zsh
# Remove a package's symlinks
stow -D -t ~ zsh
# Deploy all packages
for pkg in bash zsh tmux btop fastfetch omp rustscan scripts claude; do
stow -R -t ~ "$pkg"
doneSince all config files in $HOME are symlinks pointing to this repo, you can edit them in-place:
# Edit directly (changes are immediately in the repo)
vim ~/.zshrc
# Or edit from the repo
vim ~/linuxploitacious/zsh/.zshrc
# Both edit the same file. Commit and push when ready:
cd ~/linuxploitacious
git add -A && git commit -m "update zsh config" && git pushOn other machines, pull and restow:
cd ~/linuxploitacious && git pull
# Symlinks already point to the repo files, so changes are instant after pullThis folder is unrelated to the stow dotfiles. It contains a Docker Compose stack for separate infrastructure. It is not deployed by shellSetup.sh and can be ignored or deleted.