Quick start
From nothing to a tab-completing monom infra cloud deploy, with no config file. You'll need Linux or macOS, bash or zsh, and curl.
1. Install
$ curl -fsSL https://monom.dev/install.sh | bashThe script is about 50 lines, so read it first. It downloads the mnmd release tarball for your machine (Linux or macOS, amd64 or arm64) from GitHub releases, checks its SHA-256 against the release's checksums.txt, unpacks it into ~/.local/share/monom and links ~/.local/bin/mnmd. No sudo, no make. monom.dev serves the repo's install.sh as is, and GitHub serves the same file if monom.dev is unreachable.
Last, it runs mnmd install, which adds one source line to ~/.zshrc for zsh, or to ~/.bash_profile (falling back to ~/.bashrc) for bash. Open a new shell, or source the file it names:
monom: installed mnmd v1.2.3 in /Users/you/.local/share/monomadded to /Users/you/.zshrcrestart your shell or run: source /Users/you/.zshrcRe-running the one-liner upgrades and never adds the line twice; MONOM_VERSION=v1.2.3 pins a release. In zsh, the line has to come after compinit (or after the framework line that calls it, like source $ZSH/oh-my-zsh.sh).
No release for your platform, or working on monom itself? Build from a checkout with Go 1.24+ (no make): git clone https://github.com/adamgen/monom && cd monom && ./build.sh && bin/mnmd install. The rc line points into the checkout, so clone it somewhere permanent and don't move it; update with git pull && ./build.sh.
Setting this up with a coding agent? Point it at INSTALL.md, a step-by-step procedure with a check after every step.
2. Pick a project
Any git repository is already a monom project: its root is the project root. Outside git, mark a directory with an empty monom file. monom finds the root by walking up from wherever you are, first match wins:
$_MONOM_PROJECT_ROOT, if you've pinned one (see below);- the nearest directory containing a file named
monom; - the nearest git root.
The current directory alone is never a root.
$ mkdir acme && cd acme$ touch monom # or: git init3. Add commands
Each command is an executable file; each subfolder is a command group. Use any language: the shebang decides, and compiled binaries work too. The examples on this site all use this project:
- acme/
- db/
- migrate.pypy
- seed.jsjs
- infra/
- cloud/
- deploy.shbash
- local/
- startbash
- cloud/
- tools/
- lintgo
- Format.shsh
- _lib/
- log.shbash
- node_modules/
- README.mdmd
- release.rbrb
- db/
Start with two of its commands:
$ mkdir -p infra/cloud db$ printf '#!/usr/bin/env bash\necho "deploying to cloud..."\n' > infra/cloud/deploy.sh$ printf '#!/usr/bin/env python3\nprint("running db migrations...")\n' > db/migrate.py$ chmod +x infra/cloud/deploy.sh db/migrate.py$ mnmd discoverdb/migrateinfra/cloud/deployThat's it: there is no registration step. Default discovery registers an executable when it passes the gate:
- it starts with a shebang (
#!), or - its name looks like a command,
^[a-z0-9][a-z0-9_-]*$: lowercase, no dots. This is how compiled binaries liketools/lintget in; or - the monom file declares it.
Hidden and _-prefixed files and folders, node_modules, vendor, __pycache__, venv, target, dist, and nested monom projects are never scanned. Paths with spaces are never registered, because they couldn't be typed as words.
The file's path is the command, minus its extension: db/migrate.py is monom db migrate, and the full name, monom db migrate.py, runs it too. Only scripts, files with a shebang, lose their extension. Two scripts that share a name (build.sh and build.py) both keep theirs, and a script whose name is already taken (db.sh beside the db/ group) isn't registered, though monom db.sh still runs it; mnmd check reports both. Set discover.strip-suffix = false in the monom file to keep every extension.
Since the name rule has no dots, a file with an extension needs a shebang; without one (tools/Format.sh) it isn't registered, and mnmd check names it and says why.
4. Press Tab
$ monom <Tab>db infra$ monom in<Tab> # completes to: monom infra$ monom infra cloud deploydeploying to cloud...$ monom inframonom: 'infra' is a command groupavailable: cloudTyping a folder instead of a script lists what's inside, the same list Tab would show. Nothing runs. Commands work from any folder inside the project.
Every word you type is part of the command path, so commands don't take arguments yet: monom release v1.2 looks for release/v1.2. Flag parsing (mnmd args) is on the backlog.
5. Check it in CI
Completion stays silent when something's wrong, so it never clutters your prompt. mnmd check is the loud version. It validates the registered commands and prints one line per problem:
| Check | Severity | Reports |
|---|---|---|
root-contents | warning by default, configurable | Executables discovery found but didn't register, unreadable folders, a hook script without its execute bit. |
path-spaces | always an error | A registered path with a space in it, which completion would drop. |
declared-commands | always an error | A declared path that isn't an executable file inside the root. |
command-names | always an error | Two scripts that share a name without their extensions, or a script whose name is already taken by something beside it. |
config | always an error | An invalid line, key, or value in the monom file or config hook. |
Warnings never change the exit code, so a fresh zero-config project passes. Any error exits 1. In the full example project:
$ mnmd checkwarning: [root-contents] executable not registered: tools/Format.sh (no shebang, and the name does not match ^[a-z0-9][a-z0-9_-]*$); add a shebang, rename it, or declare it in the monom config file✔ 6 commands OK (default discovery), 1 warning(s)To make root-contents an error, set check.root-contents = error in a declarative monom file (or print it from a hook script's config hook). Set MONOM_CHECK_ROOT_CONTENTS=error to make that your default everywhere; a project's own setting wins over it.
mnmd check finds the root itself and needs no shell integration, so a CI job can install monom and run it from the project's checkout. MONOM_ACTIVE=1 silences the install hint:
curl -fsSL https://monom.dev/install.sh | bashMONOM_ACTIVE=1 ~/.local/bin/mnmd check6. The monom file
Optional. A file named monom at the root takes one of three shapes, decided by its first line:
| Shape | What it is | monom does |
|---|---|---|
| Absent | No file. | Default discovery. |
| Declarative | Any file whose first line isn't a shebang, including an empty one. | Parses it, never runs it. Default discovery plus its declarations. |
| Hook script | An executable whose first line is a shebang. | Runs it for hooks, never parses it. |
A declarative file has one entry per line. A line with = is a setting; any other line declares a command path relative to the root. Blank lines and # comments are ignored.
# register an executable the gate would skiptools/Format.sh# make mnmd check fail on unregistered executables (default: warning)check.root-contents = error# hide more entries, like dot-files (one pattern per line)discover.hide = infra/localdiscover.hide patterns are path.Match globs. One without a / matches a name at any depth; one with a / matches a path from the root, and * never crosses a /. A declared path is registered even when a pattern hides it. discover.strip-suffix = false keeps file extensions in command names; monom still runs a script by its name without one.
7. Hook scripts
When declarations aren't enough, make monom an executable with a shebang. Every hook is optional: implement one to customize a default, leave it out and monom behaves as if it weren't there.
| Hook | Does |
|---|---|
complete | Prints every command path, one per line. Output replaces default discovery; printing nothing falls back to it. |
run | Rewrites the typed words before they resolve: aliases, shortcuts, routing. |
config | Prints settings in the declarative format (check.root-contents, discover.hide, discover.strip-suffix). |
debug | Prints a writable file path for this project's debug log. |
#!/usr/bin/env bashcase "$1" in run) shift # `monom ship` runs `monom infra cloud deploy` if [ "$1" = "ship" ]; then echo "infra cloud deploy"; fi ;; config) echo "discover.hide = tools" ;; debug) echo "$(cd "$(dirname "$0")" && pwd)/.monom-debug.log" ;;esacThe run hook's exit code selects what happens:
| Hook result | monom does |
|---|---|
| exit 0, no output | Uses your original words. |
| exit 0, output | Splits the output into words and resolves those instead. |
| non-zero exit | Shows the hook's stderr, returns its exit code, runs nothing. |
Prefer if … fi over [ … ] && echo in hooks: when the test is false, && leaves exit status 1, which monom reads as a failed hook.
The repo's fixtures are working examples of every shape: demo-project has a complete hook, zero-config-project has no monom file, and zero-config-variants covers declarations, severity, discover.hide, and suffix stripping.
8. Use it from anywhere
Pre-set _MONOM_PROJECT_ROOT to skip the search and pin one project, handy for a personal ~/scripts toolbox. The pinned folder needs no monom file.
export _MONOM_PROJECT_ROOT="$HOME/scripts"Reference
| Command | What it does |
|---|---|
monom <words…> | Resolves the words to a command under the project root and runs it. |
mnmd install | Adds the shell integration to your rc file. Idempotent. |
mnmd version | Prints the installed version (the release tag, or what the build was stamped with). |
mnmd check | Validates the registered commands and the monom file. Warnings exit 0, errors exit 1. |
mnmd discover | Prints the commands default discovery registers, one path per line. |
mnmd root | Prints the active project root. |
mnmd pack <words…> | Prints the absolute path the words resolve to; a name without its extension resolves to its script. Exit 3 means “that's a command group”. |
mnmd filter <words…> | Reads paths on stdin, prints the next completions. Always exits 0. |
Troubleshooting
hint: run 'mnmd install' to activate shell integrationmnmdran in a shell that hasn't sourced monom. Run the installer, then open a new shell. SetMONOM_ACTIVE=1to silence it in scripts.monom: no project root found- No
monomfile here or in any parent, no git repository, and no pinned_MONOM_PROJECT_ROOT.cdinto a project, ortouch monomwhere the root should be. - A script is missing from Tab
- Run
mnmd check. It names executables the gate rejected and why, and scripts whose name is taken; add a shebang, rename it, or declare it in the monom file. Hidden,_-prefixed, anddiscover.hidematches are skipped on purpose. Looking fordeploy.sh? Tab lists it asdeploy. - zsh: monom runs, but Tab does nothing
- The
sourceline comes beforecompinitin~/.zshrc. Move it below. - Something else
- Set
MONOM_DEBUG_LOG=/tmp/monom.log, reproduce it, and read the log.
Going deeper? The architecture documents every contract, and the constitution explains why.