How I Set Up a Dev Machine from Scratch
The shell script I use to bootstrap any Linux or macOS machine - Rust, Node.js 24, pnpm, Git with SSH signing, and global configs in one run.
Every time I sit down in front of a new machine, I used to spend the first few hours doing the same tedious thing: installing tools one by one, copying config snippets from memory, setting up SSH keys, and wondering why pnpm wasn't on the path.
I wrote a script to fix that. One run and the machine is ready to write TypeScript, Rust, Solidity, or Cairo.
Here's exactly what it does and why.
Before You Start
You need two things before running:
- A terminal - on Linux open Terminal from your applications menu. On macOS open Terminal from Applications > Utilities.
- An internet connection - the script downloads tools and checks your GitHub SSH connection.
That's it. Everything else is handled for you.
macOS users only: The default bash on macOS is version 3, which is too old. Install a newer one first:
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
brew install bash
Then close and reopen your terminal before running the setup script.
How to Run It
curl -fsSL https://shogo-portfolio-ebon.vercel.app/scripts/dev-setup.sh | bash
If you want to read the script before running it (recommended):
curl -o dev-setup.sh https://shogo-portfolio-ebon.vercel.app/scripts/dev-setup.sh
cat dev-setup.sh
bash dev-setup.sh
The script will pause at the start of each phase and ask whether you want to run it or skip it. You stay in control the whole time. If you only need Git and nothing else, you can skip every other phase.
What the Script Installs
The script runs six phases in order.
Phase 1 - System update and essentials
On Linux it runs apt-get update and apt-get upgrade, then installs curl, wget, git, and build-essential. On macOS it installs Homebrew first if it's missing, then does brew update and brew upgrade.
This is the boring but necessary foundation. Nothing else works reliably without build-essential on Linux - it provides the C compiler that Rust, Node native modules, and several other tools require to compile.
Phase 2 - Rust via rustup
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
source "$HOME/.cargo/env"
rustup component add rustfmt clippy
I install Rust before Node because it takes longer and I want it done early. The -y flag skips the interactive prompt. After install, the script sources the Cargo env so rustc and cargo are immediately available in the same shell session without needing a restart.
rustfmt and clippy are added immediately - rustfmt formats your code, clippy catches common mistakes and style issues. There's no reason to ever skip them.
Phase 3 - Node.js 24 via nvm
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.4/install.sh | bash
nvm install 24
nvm use 24
nvm alias default 24
nvm (Node Version Manager) lets you install and switch between Node versions without needing sudo. Node 24 is the current active LTS at the time of writing. Using alias default 24 makes Node 24 the version that loads in every new terminal session automatically.
Why nvm instead of a system install? Because system Node requires sudo npm install -g, which is a security risk and causes permissions headaches. With nvm, everything lives in your home directory.
Phase 4 - Corepack, pnpm, and yarn
corepack enable
corepack prepare pnpm@latest --activate
corepack prepare yarn@stable --activate
Corepack ships with Node 16+ and manages package manager versions without a global install. This is the correct way to set up pnpm now - not npm install -g pnpm. After this phase, pnpm and yarn are both available and pinned to their latest stable versions.
pnpm is faster and more disk-efficient than npm. It stores packages once on your machine and hard-links them across projects, so you are not downloading the same dependency 30 times.
Phase 5 - Git identity, SSH key, and commit signing
This phase is the most interactive. The script prompts for your name and email, then:
- Configures
git config --global user.nameandgit config --global user.email - Checks for an existing
~/.ssh/id_ed25519key - if none found, generates one withssh-keygen -t ed25519 - Prints the public key so you can paste it into GitHub
- Waits for you to add the key before testing the connection
- Configures SSH commit signing:
git config --global gpg.format ssh
git config --global user.signingkey "$PUB_FILE"
git config --global commit.gpgsign true
git config --global tag.gpgsign true
What is a signing key? When you push a commit to GitHub, it shows "Verified" next to your commit if it's signed. This proves the commit actually came from you and wasn't tampered with. Commit signing with SSH keys is the modern approach - it uses the exact same key you already use for GitHub access, with no separate GPG setup required.
What is an SSH key? It's a pair of files: a private key (which stays on your machine and is never shared) and a public key (which you upload to GitHub). When you push code, GitHub checks your public key to confirm you're allowed to access that repository. You never type a password.
The script also sets these critical git globals:
git config --global pull.rebase true
git config --global rebase.autoStash true
git config --global fetch.prune true
pull.rebase true - instead of creating a merge commit every time you pull, git replays your local commits on top of the remote. This keeps history linear and readable.
rebase.autoStash true - if you have uncommitted changes when you pull, git automatically stashes them, does the rebase, then restores them. You never get blocked by a "please commit or stash" message.
fetch.prune true - automatically removes local references to remote branches that have been deleted. Keeps your branch list clean.
Phase 6 - Global config files
The script writes five config files to your home directory:
~/.vscode/settings.json
Format on save with Prettier, ESLint auto-fix on save, Rust analyzer with clippy, Solidity formatter, file exclusions for build output (target/, artifacts/, .next/, .nuxt/, .output/). This applies globally to every project you open in VS Code, so you never have to configure formatting per project again.
~/.prettierrc
Single quotes, trailing commas, 100 char print width, prettier-plugin-solidity included. Applied globally as a fallback for any project that doesn't have its own .prettierrc.
~/.solhint.json
solhint:recommended ruleset with warnings on long lines and missing reason strings in require calls. This runs automatically on Solidity files through the lint-staged config.
~/.lintstagedrc.json
Runs Prettier on all JS/TS/JSON/CSS/MD files before every commit. Runs both Prettier and Solhint on Solidity files. This means your code is always formatted before it reaches the remote.
~/.gitignore_global
A comprehensive global gitignore covering Node, Rust, Solidity, Python, Go, Docker, editors (VS Code, JetBrains, Vim, Sublime), OS files (macOS .DS_Store, Windows Thumbs.db), and security-sensitive files (.env*, private keys, keystores, SSH keys). Registered via:
git config --global core.excludesfile ~/.gitignore_global
This means you never accidentally commit a .DS_Store or .env file regardless of which project you're in.
After Running
- Restart your terminal - nvm and Cargo env are added to
.bashrc/.zshrcand only load in a fresh shell. - Restart VS Code if it was open when the script ran.
- Install VS Code extensions:
code --install-extension rust-lang.rust-analyzer
code --install-extension esbenp.prettier-vscode
code --install-extension dbaeumer.vscode-eslint
code --install-extension NomicFoundation.hardhat-solidity
code --install-extension Vue.volar
code --install-extension starkware.cairo1
Verifying Everything Worked
Run these after restarting your terminal:
git --version # should print git version 2.x.x
node --version # should print v24.x.x
pnpm --version # should print 9.x.x or 10.x.x
rustc --version # should print rustc 1.x.x
cargo --version # should print cargo 1.x.x
ssh -T git@github.com # should print: Hi <username>! You've successfully authenticated
If node or pnpm is not found after restarting, check that nvm added itself to your shell config:
grep -n "nvm" ~/.bashrc # Linux / bash
grep -n "nvm" ~/.zshrc # macOS / zsh
You should see a block of lines referencing ~/.nvm. If they're missing, re-run Phase 3 or add them manually from the nvm README.
What It Does Not Install
Deliberately excluded:
- VS Code itself - install it from the official source. The script only configures it.
- Docker - too varied by OS and use case to automate reliably.
- Cairo / Scarb - separate setup depending on whether you're doing Starknet development.
- Database clients - project-specific.
- Multiple Git identities - if you need to manage separate personal and work GitHub accounts from one machine, see the companion article on Git setup here.
Troubleshooting
pnpm: command not found after install
Corepack needs Node to be active. Make sure node --version returns something first, then re-run Phase 4.
ssh -T git@github.com returns "Permission denied"
The public key was not added to GitHub, or the wrong key was added. Run:
cat ~/.ssh/id_ed25519.pub
Copy that exact output, go to github.com/settings/keys, click "New SSH key", set type to "Authentication Key", and paste it in.
rustc: command not found after restart
The Cargo env is missing from your shell config. Add it manually:
echo 'source "$HOME/.cargo/env"' >> ~/.bashrc # Linux
echo 'source "$HOME/.cargo/env"' >> ~/.zshrc # macOS
Then restart your terminal.
Script exits with bash: ... syntax error
You are likely on macOS with the old system bash (version 3). Install bash via Homebrew as described in the "Before You Start" section above.