setup-system¶
setup-system takes a freshly installed machine to a working one: system
packages, an opinionated bash configuration, ssh, git defaults, and dev
tooling. It runs ephemerally via jbx — no local
installation required.
jbx setup-system --dry-run # see the whole plan, change nothing
jbx setup-system # do it
jbx setup-system -s shell # just the bash configuration
Every step is idempotent. Re-running setup-system is how you pick up a newer
template on a machine you set up months ago, not a mistake.
Steps¶
| Step | What it does |
|---|---|
deps |
Installs system packages from bootstrap.toml / jb-deps.toml in the current directory, via install-deps. Skipped when there is no deps file. |
shell |
Installs the bash configuration to ~/.config/just-bashit/ and adds one source line to ~/.bashrc and ~/.profile. |
ssh |
Tightens permissions across ~/.ssh — repairing a directory copied from Windows, FAT, a zip or git — then creates an ed25519 key named after this host if there is no key at all. Prints the public key. |
git |
Sets global git defaults that are not already set. Never touches user.name or user.email. |
tools |
Installs uv if missing; installs pre-commit hooks when the current directory is a repo with .pre-commit-config.yaml. |
claude |
Installs Claude Code if the claude command is missing. |
They always run in that order, whatever order you list them in — packages
land before the steps that need git, curl and ssh-keygen.
jbx setup-system -s shell,ssh # only these
jbx setup-system -x claude,deps # everything except these
To change the default set for a project, declare it in bootstrap.toml:
-s always overrides that, regardless of the toml setting.
The shell step¶
Your ~/.bashrc is yours. The shell step never rewrites it — it installs
two files of its own and appends one line that sources each:
~/.config/just-bashit/
bashrc.sh interactive settings — readline, history, aliases, prompt
profile.sh environment — PATH, EDITOR, ssh-agent
bashrc.d/ your additions; sourced last, never overwritten
# appended to ~/.bashrc
if [ -r "$HOME/.config/just-bashit/bashrc.sh" ]; then . "$HOME/.config/just-bashit/bashrc.sh"; fi
# appended to ~/.profile (and ~/.bash_profile, when that file exists)
if [ -r "$HOME/.config/just-bashit/profile.sh" ]; then . "$HOME/.config/just-bashit/profile.sh"; fi
Uninstalling is deleting those lines. Re-running with a newer just-bashit
replaces bashrc.sh and profile.sh in place, keeping the copy it replaced
as bashrc.sh.bak.
Why two files¶
Every distro's stock ~/.bashrc opens with an interactive-only guard:
Anything exported below that line reaches interactive terminals and nothing
else. ~/.profile runs once per login session, and everything descended
from that session inherits its exports — scripts, language servers, agents,
GUI apps — whether or not a terminal was involved. A PATH or
SSH_AUTH_SOCK set only in .bashrc therefore works when you type it and is
missing from anything that did not come from a terminal.
So environment lives in profile.sh, read by login shells, and interactive
settings live in bashrc.sh. Because terminal emulators start non-login
shells that never read ~/.profile, bashrc.sh sources profile.sh itself
when no login shell has already done so. Both paths converge; neither
duplicates the other.
profile.sh is written in POSIX sh, not bash, because ~/.profile is also
read by dash and by desktop session managers.
Put your own settings in bashrc.d/
~/.config/just-bashit/bashrc.d/*.sh is sourced last, so anything there
wins over the template and survives the next setup-system run.
What you get¶
Arrow keys search history. Type ssh and press Up: you get your last
ssh command, not your last command. Each key is bound in both normal
(\e[A) and application (\eOA) cursor modes, so it works in tmux and
screen as well as a bare terminal.
An ssh-agent that already has your keys. profile.sh adopts an inherited
agent if there is one — forwarded, systemd socket-activated, gnome-keyring,
1Password — and only starts its own if nothing is listening, on a fixed
socket path so the next login reuses it instead of leaking an agent per
shell. bashrc.sh then adds any ~/.ssh/NAME that has a matching NAME.pub
and is not already loaded, which is why passphrase prompts happen once per
key per boot rather than once per terminal.
History that survives. 100 000 entries, deduplicated, timestamped, appended rather than overwritten, and flushed at every prompt — closing one terminal never discards what another one recorded.
A prompt worth the columns. [✗2] ~/code/project (main*) $ — exit status
only after a failure, user@host only when the session is remote or
containerised, git branch from symbolic-ref and a tracked-file diff rather
than git status, which stalls the prompt in large repos.
Plus: colour ls/grep/diff probed rather than assumed (GNU takes
--color=auto, BSD takes -G), ll/la/../..., mkcd, interactive
guards on rm/cp/mv, bash 4 shell options behind a version check so
macOS's bash 3.2 does not error, and bash-completion loaded from whichever
of five paths this distro or Homebrew prefix uses.
Opt-outs¶
Set any of these in ~/.bashrc before the source line:
| Variable | Effect |
|---|---|
JB_SSH_AGENT=0 |
Do not start or adopt an ssh-agent |
JB_SSH_AUTOADD=0 |
Agent yes, but do not add ~/.ssh keys to it |
JB_PATH=0 |
Leave PATH alone |
JB_PROMPT=0 |
Leave PS1 alone |
JB_PROMPT_GIT=0 |
Prompt without the git branch |
JB_PROMPT_COLOR=0 |
Prompt without colour |
JB_READLINE=0 |
No key bindings |
JB_ALIASES=0 |
No aliases |
JB_SAFE_ALIASES=0 |
Aliases, but no interactive rm/cp/mv guards |
JB_COMPLETION=0 |
Do not load bash-completion |
The ssh step¶
Creates a key only when ~/.ssh holds no private key with a matching
.pub — a machine that already has an identity does not get another one.
The key is ed25519, named after the host, and the public half is printed
with the gh command to register it:
jbx setup-system -s ssh
# generating ed25519 key /home/you/.ssh/workstation
# ...
# gh ssh-key add /home/you/.ssh/workstation.pub --title 'workstation'
--key-name NAME overrides the filename.
Repairing a ~/.ssh that lost its permissions¶
The step also tightens permissions on everything already in ~/.ssh, before
it considers generating anything — so it doubles as the repair for a
directory that arrived from somewhere unable to carry POSIX modes. Run it on
its own:
First, check whether you need it at all. A Linux-to-Linux copy loses nothing if the transport carries modes, and then no repair is required:
rsync -a ~/.ssh/ newhost:~/.ssh/ # -a preserves permissions
tar cf - ~/.ssh | ssh newhost 'tar xf -' # tar stores them too
Modes cannot survive a crossing that has nowhere to record them, and no choice of copy command changes that:
| Crossing | Why it loses them |
|---|---|
Windows filesystem under WSL (/mnt/c/…) |
DrvFs has no POSIX modes; files read as 0777 |
| FAT / exFAT stick | the filesystem has no mode bits |
| zip | modes are optional and routinely dropped |
| a git repo | git records only the exec bit |
Those are the cases this step is for. ssh refuses a key it considers
readable by anyone else, with UNPROTECTED PRIVATE KEY FILE and no hint as
to the fix.
What it does, and deliberately does not do:
| Target | Action |
|---|---|
~/.ssh and any subdirectory |
owner gets rwx, group and other lose everything |
a private key, found by its -----BEGIN … PRIVATE KEY----- header |
group and other lose everything |
config, authorized_keys, authorized_keys2 |
group and other lose everything |
*.pub |
left alone — public by definition |
anything else, known_hosts included |
left alone |
A key is recognised by that header rather than by its name, so one with no
matching .pub, or not called id_*, is still repaired.
It only ever tightens
The sweep removes group and other bits and never adds owner bits, so a
key deliberately kept at 0400 stays 0400 instead of gaining
owner-write. Running it twice changes nothing the second time, and
--dry-run changes nothing at all.
No effect on Windows / MSYS2
MSYS2 on NTFS does not honour chmod, so the sweep is a no-op there —
it neither helps nor harms. Windows OpenSSH reads ACLs rather than mode
bits, and repairing those needs icacls, which this step does not do.
That is what ssh-to-windows is for: it publishes
WSL2's keys to %USERPROFILE%\.ssh and sets the ACLs Windows
OpenSSH requires.
The suite states this rather than hiding it: the permission tests probe
whether chmod sticks and skip on the Windows runner, where they
would otherwise assert nothing while reporting green. They run for real
on Linux and macOS.
--yes creates the key with an empty passphrase
Without --yes, ssh-keygen prompts for a passphrase as usual. With
--yes there is nothing to prompt on, so the key is written unprotected —
appropriate for a throwaway VM or a container, not for a laptop.
The git step¶
Sets these only when the key has no value already, so nothing you have chosen is overwritten:
| Key | Value |
|---|---|
init.defaultBranch |
main |
pull.rebase |
true |
rebase.autoStash |
true |
push.default |
simple |
push.autoSetupRemote |
true |
fetch.prune |
true |
diff.colorMoved |
zebra |
color.ui |
auto |
core.editor |
$EDITOR |
user.name and user.email are deliberately absent: identity belongs to
the repository, not the machine. Set it per repo with
git config user.email you@example.com.
Options¶
| Flag | Long form | Description |
|---|---|---|
-h |
--help |
Show help and exit |
-n |
--dry-run |
Print the plan; change nothing |
-v |
--verbose |
Print extra detail |
-y |
--yes |
Never prompt (see the ssh warning above) |
-s STEPS |
--steps STEPS |
Comma-separated steps to run |
-x STEPS |
--skip STEPS |
Comma-separated steps to leave out |
--prefix DIR |
Config directory (default ~/.config/just-bashit) |
|
--key-name NAME |
ssh key filename (default: this hostname) | |
--template [PATH] |
Write the bashrc template to PATH, or stdout | |
--template-profile [PATH] |
Write the profile template to PATH, or stdout |
Examples¶
Set up a new machine from scratch¶
Read the templates without installing anything¶
jbx setup-system --template # the interactive half
jbx setup-system --template-profile # the environment half
Take the shell config and nothing else¶
Provision a CI runner or container¶
Use it as your project's onboarding command¶
# bootstrap.toml
[tools.setup-system]
source = "just-bashit:setup-system"
steps = ["deps", "tools"]
[dev.apt]
packages = ["build-essential", "cmake"]
[dev.pacman]
packages = ["base-devel", "cmake"]
Dry run¶
--dry-run prints every command it would run and writes nothing at all —
no files, no config, no packages:
just-bashit setup-system
dry run — nothing will be changed
==> shell — bash configuration
would run: mkdir -p /home/you/.config/just-bashit ...
creating: /home/you/.config/just-bashit/bashrc.sh
would append to /home/you/.bashrc: if [ -r "$HOME/... "; fi
==> git — global defaults
git config --global init.defaultBranch main
...
summary
shell: ok (/home/you/.config/just-bashit)
git: ok (9 default(s) set)
Shell compatibility¶
setup-system.sh needs bash 3.2 or later — the system bash on macOS
qualifies. The installed bashrc.sh degrades on the same footing: bash 4
shell options are behind a version check, and readline settings that need a
newer readline fail silently rather than erroring.
profile.sh is POSIX sh and is safe to source from dash.