clihub
One entry point for the tools you already have.
$ ch find reshape json
jq filter and reshape JSON text
$ echo '{"name":"clihub"}' | ch jq -r .name
clihub
ch does not wrap your tools or reimplement them. It keeps a registry of names you
chose and what each one runs, so a tool you set up months ago stays reachable by
description instead of by memory.
Install
brew install nitkrar/tap/clihub # or: pipx install clihub-cli
ch init
The distribution is clihub-cli; the commands it installs are ch and
clihub. Either installer puts both on PATH and keeps them there across
upgrades, so ch init does not touch PATH — it writes config.toml with every
setting present and commented out, which is where the tunables are documented.
Installing into a virtualenv instead leaves ch in that virtualenv's bin/,
where it only resolves while the virtualenv is active:
pip install -e . # creates `ch`, inside that virtualenv only
ch init # symlinks it into ~/.local/bin as well
That is the case ch init's symlink is for. It tells you if ~/.local/bin is
not on your PATH.
Add --completions to install shell completion at the same time, or run
ch tools completion later.
On a terminal, ch init can also offer the shipped clihub skill and a
platform-matching shipped registry. Use --skill-dir DIR or --no-skill to
answer the skill offer up front.
Python 3.12+, no dependencies. It also runs straight from a checkout:
PYTHONPATH=src python3 -m clihub list
Commands
ch <name> [args...] # run a registered command
ch <group>.<name> [args...] # one inside a group
ch list [<group>] # everything registered
ch find <what you want> # by description, not by name
ch help <name> # that tool's own --help
ch doctor # is any of this going to work
ch --version
Editing the registry:
ch registry add jq --describe "filter and reshape JSON text" -- /usr/bin/jq
ch rg add llm.llama -- ~/bin/llmctl llama # a tool inside a group
ch rg edit jq --describe "..." # change one field
ch rg remove jq
ch rg {show,validate,export,import}
Everything after -- is the command and its fixed arguments. Anything you type at
run time is appended to that, untouched.
The registry
~/.clihub/registry.toml. A table is a group; a table inside it is a command in
that group. Hand-editing is expected — comments you write survive clihub rewriting
the file.
[jq]
description = "filter and reshape JSON text"
command = "/usr/bin/jq"
[llm]
description = "local model servers"
[llm.llama]
description = "start, stop and inspect the local model servers"
command = "/Users/me/bin/llmctl llama"
How a target is stored
Three ways to name one, each meaning something different:
| you write | stored as | resolved |
|---|---|---|
jq |
jq, as written |
at run time, by PATH — picks up a project's venv |
/usr/bin/jq |
as written | that exact file |
./bin/jq |
absolutised when added | — you will not be in that directory later |
A relative path is the only one rewritten, and add says so when it does.
add refuses only what can never come right — a command the shell cannot parse.
A target that is simply not here is reported and still recorded: a bare name
resolves at dispatch, and a file can be installed after the fact. ch doctor
counts that as broken and exits non-zero, so it is not lost.
Fixed arguments containing spaces are quoted for you.
Where your arguments go
command is a shell command line, run as sh -ec, so pipes, &&, redirects and
VAR=x cmd mean what they say. Your arguments bind to "$@" and are never
re-parsed — a pipe in the registry is syntax, a pipe in an argument is data.
Multi-line commands work, and are written by hand rather than through add
(argv cannot carry a script). Two rules:
- Contains
$@anywhere → used exactly as written. This is how you put arguments in the middle, or into a loop, or into one stage of a pipeline. - Otherwise
"$@"is appended — but only to a single simple command. For anything else (a pipeline,&&, a loop, several lines, a trailing comment, a here-doc) the end of the text is not an argument position, so nothing is appended. Such a command runs fine with no arguments; give it some and it stops with exit 126 rather than dropping them.ch doctorlists it as takes no arguments beforehand.
[stack.up]
description = "bring the local model stack up"
command = """
export LLM_PROFILE=dev
ch llm.vm start
ch llm.llama serve --port 8080 "$@"
"""
A trailing \ inside a """ block is TOML's own line continuation: it joins the
lines before clihub sees them, so splitting one long command across lines for
readability is still a single-line command and none of the above applies.
Configuration
~/.clihub/config.toml, written by ch init with every key present, commented
out, with its default and what it does. Uncomment what you want. Anything not in
that file is structural, not tunable.
A config that will not parse is ignored rather than fatal — dispatch carries on
with the shipped defaults, and ch doctor tells you the whole file is inert.
CLIHUB_HOME moves the directory; it is the only path variable clihub reads.
For agents
An agent has been trained on git and docker. It has not been trained on the
tools you wrote, and cannot guess they exist. That is what the registry is for.
skills/clihub/SKILL.md is the integration: point your agent at it, or copy it
where your agent looks for skills. There is no MCP server and no per-agent install
step — an agent already has a shell, and ch is a command.
For machine-readable discovery, ch list, ch find, and ch tools stats take
--json; ch registry show <name> prints TOML.
When something is wrong
ch doctor checks the registry (parsing, commands, syntax, quoting, targets,
permissions, shadowing, duplicates, portability, names) and clihub's own setup
(ch on PATH, config readable, completion current).
It never runs your tools. The one command it spawns is sh -n, which parses a
stored command and exits without executing it — that is the syntax check, and
it runs on the line clihub would actually run, "$@" included.
It exits non-zero only when something is broken — an entry that cannot run, a registry that will not parse, a config discarded whole. Advisories exit 0.
| exit | meaning |
|---|---|
| child's own | dispatch propagated it verbatim |
| 1 | a builtin failed, or (under bash as sh) a path target the shell could not run |
| 2 | usage error, or a syntax error in a stored command |
| 126 | the entry cannot take your arguments — empty, not text, or it does not say where they go |
| 127 | unknown name, or a bare target the shell could not find |
| 130 | interrupted |
Codes above 0 come from the shell once dispatch starts, and the two common sh
implementations differ for a target written as a path. Under bash as sh (macOS)
-e collapses both "missing" and "not executable" to 1; under dash (most Linux)
they stay 127 and 126. Bare names give 127 either way. clihub passes through
whatever it gets, and ch doctor names both conditions before you ever run them.
Development
python3 -m unittest discover -s tests -t . # no dependencies needed
python3 -m pytest tests/ # if you have it
AGENTS.md is how to work in this repo. docs/design.md is the specification,
docs/findings.md what testing showed about descriptions and discovery,
docs/backlog.md the open questions.
License
MIT.
Release files for clihub-cli 1.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| clihub_cli-1.1.0.tar.gz | 129.3 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| clihub_cli-1.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 244.4 kB
Release files / clihub_cli-1.1.0.tar.gz
| Download URL | clihub_cli-1.1.0.tar.gz |
|---|---|
| Size | 129.3 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
403ab4146b14041e51478055a09a564481a4f54753faba15e5378d8aef95b7a0
|
|
BLAKE2b-256 checksum How to use checksums |
67fe417243f11a10b53ff571a5ab201151c5bfe60c34ab891ae7060a9059e938
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 27, 2026.
Transparency logRelease files / clihub_cli-1.1.0-py3-none-any.whl
| Download URL | clihub_cli-1.1.0-py3-none-any.whl |
|---|---|
| Size | 115.1 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
64e6e1a73a4a706888c175aca8931901b0b660054665d1a0cf3ae226503ad516
|
|
BLAKE2b-256 checksum How to use checksums |
306a31e1d89ae88ab2cb8cd35157c4db4a6e6efdc4e7473ad87a18fc80b1bb80
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 27, 2026.
Transparency log