Skip to content

Install & shell wiring

The deep guide to getting shll and the rest of the shll toolkit onto a machine and wired into your shell. The README’s Install section is the short version; this page covers every install path and the full shll setup shell rc-wiring contract.

shll doesn’t replace the per-tool CLIs — it composes them. Everything below either shells out to brew or invokes a sub-tool’s own command; shll keeps no state of its own.

Homebrew ≥ 6.0.4 required. Homebrew 6.0 made tap-trust a hard install requirement (it defaults HOMEBREW_REQUIRE_TAP_TRUST=1), and an earlier 6.0.x Linux sandbox bug that broke trusted installs is fixed in 6.0.4. If you’re on 6.0.0–6.0.3, run brew update first.

Bootstrap via Homebrew

The recommended path is the one-liner — it bootstraps shll itself, then hands off to shll install for the rest of the roster and finishes with shll update, so the machine converges to complete and current: missing tools are installed, and already-installed tools are upgraded. It needs curl to download: minimal Ubuntu/Debian images ship without it, so there run sudo apt-get install -y curl first.

curl -fsSL https://shll.ai/install | sh

Install a subset by naming tools after sh -s --:

curl -fsSL https://shll.ai/install | sh -s -- hop wt

The script preflights the machine before touching Homebrew — it probes git (on macOS via xcode-select -p, the real Command Line Tools check; the /usr/bin/git shim false-positives when the CLT is absent), curl, and tmux, then reports every miss at once with its per-platform fix command. Missing curl (or missing git on Linux without Homebrew) is fatal; missing tmux is a warning with its install hint, never a block.

When Homebrew is absent the script bootstraps it headlessly — the official installer with NONINTERACTIVE=1 (on macOS this also installs the Command Line Tools via softwareupdate; on Linux the preflight has already guaranteed git). A fresh Homebrew isn’t on PATH in the current shell, so the script evals brew shellenv itself before handing off, and prints the rc line for your future shells — keep it (shll setup shell below wires shll’s own shell integration, not brew’s):

eval "$(/opt/homebrew/bin/brew shellenv)"   # Apple Silicon; /usr/local/bin/brew on Intel, /home/linuxbrew/.linuxbrew/bin/brew on Linux

An existing Homebrew is used as-is (≥ 6.0.4 — on 6.0.0–6.0.3, run brew update first), and the script is idempotent — safe to re-run: a re-run fills any gaps and brings the installed tools current.

The script hands off install-then-update: shll install with every arg verbatim, then exec shll update with the tool names. That has two consequences worth knowing:

  • Updating installed tools runs their update contracts — each tool’s own update side effects included (e.g. run-kit restarts its daemon). Freshly installed tools are cheap no-op updates. A failed install stops the bootstrap, so the update pass never runs over a broken install.
  • The shell integration and agent-harness steps below run automatically at the end of every bootstrap (they belong to shll install) — best-effort: a step that fails warns and prints its manual nudge instead, never failing the install — and the opt-out flags ride the same argument passthrough (curl -fsSL https://shll.ai/install | sh -s -- --no-agent-setup). Tool names ride both verbs (sh -s -- hop installs and updates only hop); flags reach shll install only — they are filtered out of the update pass.

A failed download exits 0. If the download itself fails, curl -fsSL … | sh still exits 0 silentlysh reads the empty input and succeeds — so an &&-chained next step proceeds as if the install worked. Curl’s error does appear on stderr (that’s the -S), but the pipeline’s exit code cannot be trusted. (The script’s main() wrapper protects against partial execution of a truncated download, not against a failed one.) After a run that seemed to do nothing, check command -v shll — or re-read stderr — before chaining on.

Always tap-qualify formula names. homebrew/core now carries an unrelated run-kit formula — a bare brew install run-kit installs someone else’s software. Every toolkit formula is sahil87/tap/<formula> (sahil87/tap/run-kit, sahil87/tap/shll, …).

If you’d rather bootstrap by hand, the manual equivalent is trust-then-install for shll itself:

brew trust --formula sahil87/tap/shll && brew install sahil87/tap/shll

The brew trust is required: shll’s tap formula downloads a binary and runs a sandboxed def install (not a bottle pour), and that sandboxed step re-checks trust against a real persisted trust record — so naming the formula on the CLI alone is refused on Homebrew 6.0+ (see Tap-trust troubleshooting for the load-gate vs. sandboxed-install-gate detail). shll can’t trust its own formula before it exists, which is why this one-time bootstrap uses brew trust directly; from there shll install owns trust for the other six roster tools.

shll install — bootstrap the missing roster tools

shll install

Iterates the hardcoded roster — run-kit, rk-desktop, fab-kit, wt, idea, tu, hop — and, for each brew-managed tool you don’t already have, records per-formula Homebrew trust (brew trust --formula sahil87/tap/<formula>) before running brew install sahil87/tap/<formula>. On Homebrew 6.0+ trust is a hard install requirement, so trusting first is what lets the install proceed; brew trust is idempotent, so re-runs stay clean. Already-installed tools are skipped silently. It is idempotent — safe to re-run; a second run picks up only tools added since the first.

rk-desktop (the run-kit desktop viewer shell) is the roster’s one non-brew entry: there is no formula, so there is no trust step — shll install delegates to rk desktop install instead. It is actionable only when rk (run-kit) is installed and the platform supports the desktop app; on an unsupported platform (or with rk missing) it is skipped with a note, never a failure — and a targeted shll install rk-desktop prints the refusal explicitly. In a whole-roster run it is processed right after run-kit, and if run-kit’s install failed that run, rk-desktop is skipped too.

This is Homebrew’s recommended per-formula trust granularity for third-party taps — shll knows its exact roster, so it trusts only what it actually manages (not the whole tap).

shll install --no-trust    # skip the per-formula trust step (manage trust yourself)

If your Homebrew is too old to ship brew trust (pre-6.0, where trust isn’t required anyway), the trust step is skipped gracefully and the install proceeds.

When the installs finish (or there was nothing to do), shll install wires the machine automatically — no nudges to ignore, no prompts:

  1. Shell integration — the equivalent of shll setup shell: the eval "$(shll shell-init <shell>)" block is appended to your rc file (sentinel-managed and idempotent, so a re-run is a no-op), followed by an exec $SHELL reminder. Already-wired, unresolvable-$SHELL, and corrupt-block states skip quietly.
  2. Agent harnesses — the equivalent of shll setup agent --yes: the shll-toolkit skill is placed at the two global skill paths and run-kit’s dashboard hooks are delegated, with --yes forwarded so run-kit’s hook-wiring confirmation can’t hang an unattended (curl | sh) run.

Both steps are best-effort: a failure warns on stderr and prints that step’s manual nudge as a fallback, and never changes the install’s exit code. Neither step runs under --dry-run. Opt out with --no-shell-setup (dotfile-manager users) and/or --no-agent-setup:

shll install --no-shell-setup   # skip the rc-file wiring (wire it yourself)
shll install --no-agent-setup   # skip the agent-harness wiring

Both halves are re-runnable any time via shll setup (or individually as shll setup shell / shll setup agent) — e.g. after installing a new shell or a new agent harness.

shll install does not upgrade — it only installs what’s missing. Use shll update for upgrades. It also runs no brew update --quiet first: brew install resolves the formula via the tap directly, so the metadata refresh that shll update performs is intentionally absent here.

You can also target a subset by name: shll install hop wt installs just those two (in roster order, regardless of arg order). shll itself is not a valid install target — you can’t brew install the running orchestrator (it’s the one-time bootstrap above).

Requires Homebrew. If brew isn’t on PATH, shll install prints shll install requires Homebrew. Install from https://brew.sh and exits 1.

From source

git clone https://github.com/sahil87/shll.git
cd shll
just install

just install builds the binary and copies it to ~/.local/bin/shll. Make sure that directory is on your $PATH. A from-source build participates in shll shell-init and shll version exactly like a brew install — install detection is by binary-on-PATH, not by brew. One caveat: a non-brew shll is not self-upgraded by shll update (there’s no brew formula to upgrade), and it reports its own version as whatever the build stamped (dev for an unstamped local build).

shll setup shell maintains a single sentinel-wrapped, shll-managed block in your shell rc file. The block holds the cross-tool eval line — that’s all. It is pure rc-wiring and touches no Homebrew state (tap trust lives in shll install, which trusts each formula it installs). It is the recommended way to wire your shell: you don’t have to know which rc file to edit, and re-running is a no-op.

Renamed from shll shell-setup: the old spelling (and its shll shell-install alias) still works — hidden, silent, for one release cycle — then it will be removed.

shll setup shell                          # auto-detect shell from $SHELL, append the eval block
shll setup shell zsh                      # explicit shell
shll setup shell --print                  # dry-run: print the block to stdout, modify nothing
shll setup shell --uninstall              # clean removal of the whole block
shll setup shell --rc-file ~/.zshrc.local # override the target path verbatim

The managed block

The block is bookended by sentinels and is idempotent — re-running is a no-op when the line is already present:

# >>> shll >>>
eval "$(shll shell-init zsh)"
# <<< shll <<<

The eval line is the cross-tool composition entry point — it runs shll shell-init at shell startup.

Shell auto-detection and rc-file targets

With no positional argument, shll setup shell infers the shell from the basename of $SHELL (so /bin/zsh and /usr/local/bin/zsh both resolve to zsh); pass zsh/bash explicitly to override. Default rc targets:

ShellDefault rc file
zsh${ZDOTDIR:-$HOME}/.zshrc
bash (macOS)$HOME/.bash_profile
bash (Linux)$HOME/.bashrc

--rc-file <path> short-circuits derivation entirely and writes to the path you name — the escape hatch for $ZDOTDIR users, dotfile managers, and CI.

The fresh-block append uses plain O_APPEND, so a ~/.zshrc symlink into a dotfile manager (chezmoi, dotbot, stow, yadm) stays a symlink and the source-of-truth file receives the block. shll never creates an rc file: if the target doesn’t exist it tells you so and exits rather than masking a misconfigured $ZDOTDIR or a dotfile manager that hasn’t applied yet.

Migrating from an older shll (--trust-tap cleanup)

Older shll versions had a shll shell-setup --trust-tap flag that also wrote an export HOMEBREW_REQUIRE_TAP_TRUST=1 policy line into the block. That flag is removed — trust now lives in shll install (per-formula), and the export line merely re-set Homebrew 6.0’s default (it was never what unblocked installs; the brew trust record is). If a previous --trust-tap run left that export line in your block, the next plain shll setup shell run rewrites the block to the eval line only, dropping the stale export automatically. --uninstall removes the whole block as before.

shll shell-init <shell> — the composed eval line

If you’d rather wire the eval line by hand, this is exactly what shll setup shell writes to your rc file:

eval "$(shll shell-init zsh)"   # in ~/.zshrc
eval "$(shll shell-init bash)"  # in ~/.bashrc

The output is the concatenation, in roster order (run-kit, rk-desktop, fab-kit, wt, idea, tu, hop), of every installed shll tool’s own shell-init. What each tool contributes:

ToolWhat it adds to your shell
run-kitcompletion
rk-desktop— (no shell integration; the desktop app)
fab-kitcompletion
wtwt shell function wrapper (so the “Open here” menu option can cd your shell), completion
ideacompletion
tucompletion
hophop shell function (bare-name cd, verb dispatch, tool-form), h / hi aliases, completion

hop and wt are the only tools that ship shell functions — those need eval-time installation because a function defined inside a binary can’t escape into the parent shell. Everything else is completion, sourced lazily on tab. The output is always eval-safe: a tool that isn’t installed is silently omitted, and a tool whose shell-init errors has its output dropped (the error goes to stderr only) — so a broken sub-tool never corrupts your shell. See Composing shell-init for the composition mechanics.

Tap-trust troubleshooting

On Homebrew 6.0+, trusting sahil87/tap is a hard install requirement — not the advisory “allowed by default” warning older Homebrew printed. Homebrew now defaults HOMEBREW_REQUIRE_TAP_TRUST=1, so brew install sahil87/tap/<formula> is refused until a real trust record exists. If you skip the bootstrap, brew refuses the install — often as an opaque sandbox build failure rather than a clear “untrusted tap” message.

Why naming the formula on the CLI isn’t enough — the two trust gates. Trust is checked in two places:

  1. At formula-load time, outside the sandbox — here naming the fully-qualified formula (sahil87/tap/shll) on the command line is explicitly allowed.
  2. Again during the sandboxed install — and that in-sandbox re-check sees the formula’s path, not the qualified name you typed, so CLI-naming does not satisfy it. A persisted trust record (tap- or formula-level) is genuinely required.

shll’s tap formulae download a binary and run a sandboxed def install — they are not bottle do bottles (a true bottle pour runs no sandboxed install and would need no pre-trust). So the second, sandboxed gate always fires for these formulae, which is exactly why the bootstrap brew trust line is required.

The fix — bootstrap, then let shll install handle the rest:

brew trust --formula sahil87/tap/shll && brew install sahil87/tap/shll   # one-time bootstrap for shll itself
shll install                                                             # trusts each remaining formula, then installs

shll install runs brew trust --formula sahil87/tap/<formula> before each install (per-formula — Homebrew’s recommended granularity for third-party taps). brew trust is idempotent, so re-running any of this is safe. Pass shll install --no-trust if you’d rather manage trust yourself.

Already installed but a later brew upgrade / shll update is refused? A tool installed outside shll install (manually, or before this feature) may be untrusted, and Homebrew 6.0+ refuses its next upgrade. shll doctor flags any installed-but-untrusted tool with WARN (read-only, via brew trust --json=v1); re-run shll install (idempotent) or brew trust --formula sahil87/tap/<x> to fix it.

Homebrew version floor. Requires Homebrew ≥ 6.0.4 — 6.0.0–6.0.3 on Linux had a bubblewrap-sandbox bug that broke trusted installs (the sandbox couldn’t read the trust file), fixed in 6.0.4. On an earlier 6.0.x, run brew update first.

See also

  • Workflows — clean-machine bootstrap, day-to-day shll update, version dumps, and the composition model.
  • shll.ai — the always-current command reference (CI publishes shll’s help tree on every release).
  • github.com/sahil87/shll — the source repository.