Part of the shll toolkit — see all projects there.
One command to install, update, and shell-wire every tool in the shll toolkit (wt, idea, tu, run-kit, hop, fab-kit). shll doesn't replace the per-tool CLIs — it composes them.
From a clean machine to a fully wired toolkit:
curl -fsSL https://shll.ai/install | sh # install shll + the whole roster
shll shell-setup # wire shell integration into your rc file
shll agent-setup # optional, once per machine: place the toolkit skill for agent harnesses
exec $SHELL # reload so the shell integration takes effectInstall a subset by naming tools after sh -s --:
curl -fsSL https://shll.ai/install | sh -s -- hop wtRequires Homebrew ≥ 6.0.4 (on 6.0.0–6.0.3, run brew update first); the script never auto-installs Homebrew. It's idempotent — safe to re-run, and a no-op for anything already installed.
The shll agent-setup line is optional and once per machine — it places one thin shll-toolkit Agent Skill at the harnesses' global skill paths so an agent driving this machine knows to load shll skill before reaching for a tool, and delegates dashboard hooks to run-kit's agent-setup. Skip it if you don't drive this machine with agents.
Everything else — the manual brew bootstrap, from-source builds, shell-wiring detail, and tap-trust troubleshooting — lives in the install guide on https://shll.ai.
- One-shot install —
shll installrecords per-formula Homebrew trust and then runsbrew install sahil87/tap/<formula>for every roster tool you don't already have. Idempotent and safe to re-run. - One-line shell integration —
shll shell-setupappends a single eval line to your rc file that wires uphop,wt, and any future toolkit shell-init in one block. No more managing four eval lines. - One update for everything —
shll updaterunsbrew updateonce, then upgrades every installed roster tool in sequence. Skips ones you don't have. Skips itself if it wasn't installed via brew. - Paste-friendly version dump —
shll versionprints one row per tool, ideal for bug reports. - At-a-glance roster —
shll listshows every managed tool with its install status, a one-line description, and its repo (plus--jsonfor scripting). - One-command health check —
shll doctorverifies each tool is installed, runnable, and shell-wired, with an actionable fix on every problem line.
Per-tool CLIs continue to work standalone — shll wraps them, it does not replace them.
shll install # trust + install every missing roster tool
shll install hop wt # install only a named subset
shll install --no-trust # skip the per-formula trust step
shll install --dry-run # preview the brew install plan, change nothingIterates the roster in leaves-first order (wt, idea, tu, run-kit, hop, fab-kit) and, for each one that's missing, records per-formula Homebrew trust (brew trust --formula sahil87/tap/<formula>) before running brew install sahil87/tap/<formula>. Homebrew 6.0 makes tap-trust a hard install requirement, so this is what lets the install proceed; brew trust is idempotent, so re-runs stay clean. Already-installed tools are skipped silently. Does NOT upgrade — use shll update for that.
Pass --no-trust to skip the trust step entirely (for users who manage trust themselves). 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.
Pass one or more tool names to install only that subset (processed in roster order regardless of arg order); an unknown name is a hard error. Unlike shll update, shll itself is NOT a valid install target — you can't brew-install the running orchestrator (it's the one-time bootstrap brew trust --formula sahil87/tap/shll && brew install sahil87/tap/shll). --dry-run runs the read-only install-status probes, prints the exact brew install commands the real run would execute, then exits without installing anything.
Each install prints a [N/M] progress header, and a timing summary tail closes the run.
shll update # upgrade shll + every installed roster tool
shll update shll # upgrade only shll itself
shll update hop wt # upgrade only a named subset
shll update --dry-run # preview the upgrade plan, change nothingRuns brew update --quiet once, then brew upgrade sahil87/tap/shll (when shll itself was installed via brew), then delegates to each installed roster tool's own update subcommand (passing --skip-brew-update when the tool advertises it) so each tool's post-upgrade side effects — e.g. run-kit's daemon restart — are preserved. A roster tool that exposes no update subcommand falls back to brew upgrade sahil87/tap/<formula>. Uninstalled tools are skipped silently, and the loop is best-effort — one tool's failure doesn't abort the rest. Brew and per-tool progress stream directly to your terminal.
When agent skills were previously placed via shll agent-setup, the run ends by re-running shll agent-setup (as a subprocess, so the freshly upgraded binary places its own skill content) — the placed skills track the toolkit they describe without a manual follow-up. The refresh is placement-gated (a machine that never opted in gets no writes), best-effort (it never changes the run's exit code), and previewed by --dry-run.
Legacy
rkkeg exception. On a pre-rename machine still holding the oldrkkeg,shll updatemigratesrun-kitbrew-direct (brew upgrade sahil87/tap/rk, which resolves the rename) rather than delegating torun-kit update. Because that path skipsrun-kit update's post-upgrade side effect, the daemon is not restarted automatically — shll prints a note suggestingrun-kit serve --restartinstead (Constitution III — shll never reimplements a tool's own logic). This is transitional; once legacy kegs are gone, updates always take the normal delegated path above.
Pass one or more tool names to scope the run to a subset (valid targets: shll, wt, idea, tu, run-kit, hop, fab-kit; the legacy alias rk still resolves to run-kit), processed in roster order regardless of arg order. A named-but-not-installed target is a hard error here (unlike the whole-roster sweep, which silently skips it). --dry-run runs the read-only probes, prints the exact commands the real run would execute (shll (self) first when brew-installed), then exits without writing anything.
Each tool gets a [N/M] progress header, and a timing summary tail (Done — N of M tools succeeded in <dur>.) closes the run. After the tail, a compact "What changed:" digest lists the release-note titles for every tool that actually bumped, followed by a copy-pasteable shll changelog command for the full notes.
shll check-updates # human table: installed → latest per tool
shll check-updates --json # machine contract (what run-kit's daemon runs)
shll check-updates --source github # compare against GitHub release tags insteadThe toolkit's single update-check surface: for shll itself plus every roster tool, it reports the installed version vs the latest available — and never updates anything (that's shll update's job). One backend, selected by --source: --source released (the default when the flag is omitted) fetches shll.ai/versions.json, the roster + notify-policy authority, once per run; --source github reads each tool's latest GitHub release tag instead (no notify policy in that backend). Installed versions come from Homebrew, so brew must be present.
The human output is a shll version-style aligned table — shll 0.1.5 → 0.1.6 update available (notable), wt 0.1.3 up to date, idea not installed. The (notable) marker means the pending bump crosses the tool's notify threshold from the manifest (patch = any bump is notable; minor = only minor-or-higher bumps; never = none).
--json emits a stable machine contract — {"schema": 1, "source": "released", "tools": [{"name", "formula", "installed", "latest", "notify", "update_available", "notable"}]} — with a row only for tools where both versions resolved (not-installed or unresolvable tools are omitted; --source github rows omit notify/notable since no policy source exists there). The contract evolves additively — consumers tolerate unknown fields. Exit codes: 0 when the check ran (pending updates don't change it — verdicts live in the output), 1 when the check itself failed (manifest unreachable, brew missing), 2 on a usage error; a --source github per-tool fetch failure degrades just that tool and still exits 0.
shll changelog # all installed tools: installed → latest ("what would an update bring?")
shll changelog tu # one tool: installed → latest
shll changelog tu@0.6.2..0.6.4 # explicit range: releases in (0.6.2, 0.6.4]
shll changelog tu@0.6.2..0.6.4 hop@0.1.16..0.1.18 # multiple tools (this is what `shll update` prints)Fetches each tool's GitHub release notes (unauthenticated, via the public API) and prints them newest-first: a per-tool {tool} {old} → {new} ({N} releases) header followed by each release's tag, title, and full "What's Changed" body. With no arguments it previews the pending releases for every installed tool (shll itself first) — its installed version → the latest release. Name one or more tools to scope it, or add an explicit tool@old..new range (which works regardless of what's installed). Versions are accepted with or without a leading v.
Valid targets are the roster names plus shll itself. A no-range form needs Homebrew to read the installed version; an explicit range skips brew entirely. If a fetch fails (rate limit, network), that tool degrades to a Full Changelog compare URL and the command still exits 0 (Constitution V — graceful degradation). Output is capped at the 10 most recent releases per tool, with a compare URL for the overflow.
Still works under the legacy alias
shll shell-install— same command, unchanged behavior.
shll shell-setup # auto-detect shell, append eval block to your rc file
shll shell-setup --print # dry-run: print the block to stdout, modify nothing
shll shell-setup --uninstall # clean removal of the block
shll shell-setup --rc-file ~/.zshrc.local # override the target pathshell-setup is pure rc-wiring — it maintains only the eval line and touches no Homebrew state. (Tap trust lives in shll install, which trusts each formula it installs; there is no --trust-tap flag.) The appended block is sentinel-wrapped and idempotent — re-running is a no-op when the line is already present:
# >>> shll >>>
eval "$(shll shell-init zsh)"
# <<< shll <<<The rc file is opened with plain O_APPEND, so dotfile-manager symlinks (chezmoi, dotbot, stow, yadm) are preserved. Default targets: ${ZDOTDIR:-$HOME}/.zshrc for zsh, $HOME/.bash_profile (macOS) or $HOME/.bashrc (Linux) for bash.
Upgrading from an older shll? If a previous
shll shell-setup --trust-tapleft anexport HOMEBREW_REQUIRE_TAP_TRUST=1line in your block, the nextshll shell-setuprun cleans it out automatically (the block is rewritten to the eval line only). That export merely re-set Homebrew 6.0's default and was never what unblocked installs — thebrew trustrecord is.--uninstallremoves the whole block as before.
If you'd rather wire the eval line by hand, this is what shll shell-setup writes to your rc file:
eval "$(shll shell-init zsh)" # in ~/.zshrc
eval "$(shll shell-init bash)" # in ~/.bashrcThe output is the concatenation (in roster order — leaves-first: wt, idea, tu, run-kit, hop, fab-kit) of every installed shll tool's own shell-init, with a # ── <tool> ── comment separator before each block. What each roster tool is for, and what it adds to your shell:
| Tool | What it's for | What it adds to your shell |
|---|---|---|
wt |
git worktree manager — create, switch, and clean up worktrees | wt shell function wrapper (so the "Open here" menu option can cd your shell), completion |
idea |
worktree-aware idea / backlog capture from the terminal (markdown-first) | completion |
tu |
AI coding-assistant cost/usage tracker (Claude Code, Codex, OpenCode) | completion |
run-kit |
web-based tmux orchestration for parallel agent workspaces (formerly rk, which stays as an alias) |
completion |
hop |
fast directory navigation / bookmarks (cd on steroids) |
hop shell function (bare-name cd, verb dispatch, tool-form), h / hi aliases, completion |
fab-kit |
fab — spec-driven change workflow (this repo's own pipeline) |
completion |
hop and wt are the only tools that ship shell functions — those need eval-time installation because a function defined inside the binary can't escape into the parent shell. Everything else is completion, which the shell sources lazily on tab. The output is 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). Per-tool <tool> shell-init <shell> continues to work standalone if you'd rather wire them up individually.
$ shll version
shll v0.0.5
wt v0.0.5
idea v0.0.2
tu v0.4.13
run-kit v1.5.3
hop v0.1.5
fab-kit v1.9.4One row for shll itself plus each roster tool, in roster order. Uninstalled tools render as not installed. Each tool's --version call has a 2-second timeout, so one hung tool can't block the dump — a timeout also shows as not installed. Drop the whole block into a bug report.
$ shll list
ok shll the manager for the shll toolkit https://github.com/sahil87/shll
ok wt Git worktree management — create, list, open, delete worktrees https://github.com/sahil87/wt
ok idea Backlog idea management from the terminal https://github.com/sahil87/idea
ok tu Token-usage tracker for AI coding tools (Claude Code, Codex, OpenCode) https://github.com/sahil87/tu
ok run-kit Run-kit — tmux session manager with a web UI; can display web pages/HTML to the user and push notifications (rk stays as an alias) https://github.com/sahil87/run-kit
ok hop Fast directory/project jumping across worktrees https://github.com/sahil87/hop
ok fab-kit Spec-driven workspace & workflow toolkit (the `fab` CLI) https://github.com/sahil87/fab-kitshll leads, then one row per managed tool in roster order: an install-status marker (ok / --, or a green ✓ / red ✗ on a terminal), the name, a one-line description, and the source-repo URL. The leading shll row is the manager itself — it's surfaced so the toolkit reads as one family with shll as its manager-member (the same shll-first ordering shll version and shll update already use). Install status reuses the same PATH probe as shll version (it's install-mechanism agnostic, not a Homebrew check); a missing tool is shown as missing, never an error, so shll list always exits 0.
shll list --json # JSON array, no color — pipe into jq--json emits a {name, description, repo, installed} array (repo is the full resolved URL), suitable for shll list --json | jq. The leading shll object additionally carries "self": true (absent on the six managed tools), so a script driving brew install can recover just the managed set with jq 'map(select(.self != true))'.
$ shll doctor
shll OK v0.0.16
wt OK v0.0.16 wired
idea OK v0.0.7
tu OK v0.4.17 wired
run-kit OK v2.2.3
hop OK v0.1.16 wired
fab-kit OK v2.1.1shll leads with its own row (it's the running binary; version from the build, no wiring or trust check — but it does verify any agent skill placed by shll agent-setup: a placement whose content is stale, typically from an older shll, is WARN with a refresh pointer; no placement means no check). Then for every roster tool doctor checks that (1) the binary is on PATH, (2) it reports a version (so a half-installed or stale brew link is caught), (3) its Homebrew formula is trusted (so a future brew upgrade won't be refused on Homebrew 6.0+), and (4) — for the tools that ship shell integration (wt, tu, hop) — shll's composed eval block is present in your rc file. Each tool gets one line with an OK / WARN / FAIL marker, and every non-OK line carries an actionable suggestion (e.g. run 'brew install …'; formula not trusted — run 'shll install' …; or not wired — run 'shll shell-setup' then 'exec $SHELL'). The shll row can at worst WARN, so it never affects the exit code.
A missing or non-running binary is FAIL; an installed-but-untrusted or installed-but-unwired tool is WARN (it still works when invoked directly — but an untrusted tool's next upgrade will be refused). The trust sub-check queries brew trust --json=v1 read-only (it never reads ~/.homebrew/trust.json directly) and is skipped silently when your Homebrew is too old to ship brew trust. doctor is strictly read-only — it never installs, upgrades, trusts, or edits your rc file — and it exits non-zero if any tool is FAIL, so it's scriptable in CI. Pass --json for a machine-readable array (one object per tool) under the same checks and exit contract.
$ shll standards
principles foundation The ten toolkit CLI principles every tool is built against
help-dump binary Machine-readable help contract every tool must emit
readme-extraction repo README + docs/site structure standard for toolkit repos
skill binary+repo Agent skill bundle standard: docs/site/skill.md served by `<tool> skill`
$ shll standards principles # print the full document (raw markdown, stdout)The agent-facing reader for the toolkit's standards (docs/site/standards/principles.md and companions). The bare form is a self-describing glossary — an agent that has only been told "run shll standards" can pick the right document from the list alone, with a scope column (foundation / binary / repo / binary+repo) naming where each standard's obligations live; shll standards <name> prints that document byte-identical to its canonical docs/site/standards/ source. Content is embedded at build time, so it's offline and versioned with the release (a drift-guard test keeps the embedded copies byte-matched to docs/site/standards/). Pass --json on the bare form for a {name, description, scope, source_path} array (source_path is docs/site/standards/<name>.md); an unknown name errors on stderr naming the valid names and exits non-zero.
$ shll skill # glossary: one line per installed tool (shll first)
shll the manager for the shll toolkit
wt Git worktree management — create, list, open, delete worktrees
hop Fast directory/project jumping across worktrees
Run 'shll skill <tool>' for that tool's full agent skill bundle ('shll skill <tool> <topic>' for a topic page).
$ shll skill hop # print hop's full agent skill bundle (raw markdown)
$ shll skill run-kit display # print one of a tool's topic pages (passed through to `run-kit skill display`)
$ shll skill shll # shll's own bundle (served from the embedded copy)The agent-facing reader for each tool's offline skill bundle — the one-page usage briefing an agent loads before driving the tool (per the toolkit's skill standard). The bare form is a glossary, not a dump of every bundle: it lists the installed tools one line each (shll first, then the roster, PATH probe only — no brew calls), never concatenating bundles, so an agent picks the one it needs and asks for it by name. shll skill <tool> streams that tool's own <tool> skill output byte-for-byte (shll skill shll serves shll's own bundle from an embedded copy, drift-guarded against docs/site/skill.md). A tool that isn't installed, or whose version predates its skill subcommand, prints a one-line notice to stderr and exits 1; an unknown tool name is a usage error (exit 2).
shll agent-setup # place the shll-toolkit skill at both locations (idempotent)
shll agent-setup --print # print the SKILL.md content and both target paths, write nothing
shll agent-setup --uninstall # remove both placed skill directoriesMechanically places one thin shll-toolkit Agent Skill into the harnesses' global skills directories — ~/.agents/skills/shll-toolkit/SKILL.md (the agentskills.io open-standard path, read by Codex and compat-read by Cursor and OpenCode) and ~/.claude/skills/shll-toolkit/SKILL.md (Claude Code, which doesn't read ~/.agents/) — so an agent driving this machine learns to load shll skill before reaching for a tool. The skill directories are shll-owned, so placement is idempotent by construction: install writes them, a re-run overwrites them, --uninstall deletes them — no merge, no prompt, no sentinel machinery. A per-path written/updated/unchanged summary is printed. Then it delegates run-kit's dashboard-hook wiring to run-kit agent-setup (skipped silently when run-kit isn't installed; --print/--uninstall don't delegate a placement). This graduates the toolkit's harness wiring from run-kit agent-setup, where it was mis-homed on a leaf tool, up to the manager.
Once placed, the skill maintains itself: shll update ends each run by re-running agent-setup (so the placed content tracks the upgraded binaries), and shll doctor flags a stale placement with a WARN. The skill's frontmatter description is generated from the tool roster — each tool contributes its name and a task-domain phrase ("git worktrees", "backlog ideas") so agents match on the task, not just the tool name, and run-kit additionally contributes an agent-proactive sentence (show visual content in a browser window, push notifications) so agents reach for those capabilities unprompted.
shll has no state, no database, and no special knowledge of the tools it wraps. Every subcommand is a thin coordinator over the per-tool CLIs:
shll command |
What it actually runs |
|---|---|
shll install |
brew trust --formula sahil87/tap/<formula> then brew install sahil87/tap/<formula> per missing tool (--no-trust skips the trust step) |
shll update |
brew update --quiet once, self-upgrade, then each installed tool's own update (delegated; brew upgrade fallback only when a tool has no update) |
shll check-updates |
fetches shll.ai/versions.json (or GitHub releases with --source github), joins against brew list --versions, reports pending updates — read-only |
shll changelog |
fetches each tool's GitHub releases (public API), filters to the requested version range, renders the notes |
shll shell-init zsh |
concatenates the stdout of each installed tool's <tool> shell-init zsh |
shll version |
invokes <tool> --version per tool, formats as a table |
shll list |
probes each tool's install status, renders the roster (name, description, repo) |
shll doctor |
probes <tool> --version + reads your rc file, reports install + wiring health |
shll standards |
prints build-time-embedded copies of the canonical docs/site/ standards (no subprocess, no network) |
shll skill <tool> |
passes through the tool's own <tool> skill output byte-for-byte (shll skill shll serves an embedded copy) |
shll agent-setup |
places the shll-toolkit skill at the two global skill paths, then delegates run-kit agent-setup for run-kit's hooks |
Per Constitution Principle IV (Composition, Not Replacement): hop update, wt shell-init, etc. continue to work standalone. shll's only job is to fan-out, collect output, and degrade gracefully when a tool is missing.
- docs/site/install.md — install & shell-wiring guide (manual brew bootstrap, from-source,
shll shell-setup, tap-trust) - docs/site/workflows.md — task-oriented walkthroughs (clean-machine bootstrap, day-to-day
shll update, version dumps, the composition model) - docs/site/standards/principles.md — the ten CLI principles every toolkit tool is built against (agent-native contracts: obligations, failure modes, enforcement receipts)
- docs/site/standards/help-dump.md — producer standard for the machine-readable help contract (
help-dumpJSON every tool must emit) - docs/site/standards/readme-extraction.md — producer standard for README &
docs/site/structure (what shll.ai pulls and renders per tool) - docs/site/standards/skill.md — producer standard for the offline, embedded
<tool> skillagent bundle (one-page usage briefing, versioned with the binary) - docs/site/standards/update.md — producer standard for the in-place
updatesubcommand (--skip-brew-updateprobe, exit-code semantics, brew-handling safety, naming/release alignment) - docs/site/standards/version.md — producer standard for the
--versionsurface shll probes (2s budget, first-non-empty-line token, binary-name-equals-tool install probe) - docs/site/standards/shell-init.md — producer standard for the eval-safe
shell-initoutput shll concatenates (stdout-only shell source, diagnostics to stderr, fail-non-zero) shll --help— full subcommand listing- Command reference at shll.ai/shll/commands — a browsable, always-current command tree. On every release, shll's CI exports its CLI help tree as a machine-readable
help/shll.jsonand publishes it to shll.ai, which renders it at that page. The export is produced by a hiddenhelp-dumpsubcommand (internal build tooling, not a user command). - Per-tool repos for the wrapped CLIs: fab-kit · run-kit · tu · hop · wt · idea