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 | bash

The 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/monom
added to /Users/you/.zshrc
restart your shell or run: source /Users/you/.zshrc

Re-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:

  1. $_MONOM_PROJECT_ROOT, if you've pinned one (see below);
  2. the nearest directory containing a file named monom;
  3. the nearest git root.

The current directory alone is never a root.

$ mkdir acme && cd acme
$ touch monom # or: git init

3. 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
    • tools/
      • lintgo
      • Format.shsh
      • bash
    • README.mdmd
    • release.rbrb

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 discover
db/migrate
infra/cloud/deploy

That's it: there is no registration step. Default discovery registers an executable when it passes the gate:

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 deploy
deploying to cloud...
$ monom infra
monom: 'infra' is a command group
available: cloud

Typing 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:

CheckSeverityReports
root-contentswarning by default, configurableExecutables discovery found but didn't register, unreadable folders, a hook script without its execute bit.
path-spacesalways an errorA registered path with a space in it, which completion would drop.
declared-commandsalways an errorA declared path that isn't an executable file inside the root.
command-namesalways an errorTwo scripts that share a name without their extensions, or a script whose name is already taken by something beside it.
configalways an errorAn 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 check
warning: [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 | bash
MONOM_ACTIVE=1 ~/.local/bin/mnmd check

6. The monom file

Optional. A file named monom at the root takes one of three shapes, decided by its first line:

ShapeWhat it ismonom does
AbsentNo file.Default discovery.
DeclarativeAny file whose first line isn't a shebang, including an empty one.Parses it, never runs it. Default discovery plus its declarations.
Hook scriptAn 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.

acme/monom
# register an executable the gate would skip
tools/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/local

discover.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.

HookDoes
completePrints every command path, one per line. Output replaces default discovery; printing nothing falls back to it.
runRewrites the typed words before they resolve: aliases, shortcuts, routing.
configPrints settings in the declarative format (check.root-contents, discover.hide, discover.strip-suffix).
debugPrints a writable file path for this project's debug log.
acme/monom
#!/usr/bin/env bash
case "$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"
;;
esac

The run hook's exit code selects what happens:

Hook resultmonom does
exit 0, no outputUses your original words.
exit 0, outputSplits the output into words and resolves those instead.
non-zero exitShows 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

CommandWhat it does
monom <words…>Resolves the words to a command under the project root and runs it.
mnmd installAdds the shell integration to your rc file. Idempotent.
mnmd versionPrints the installed version (the release tag, or what the build was stamped with).
mnmd checkValidates the registered commands and the monom file. Warnings exit 0, errors exit 1.
mnmd discoverPrints the commands default discovery registers, one path per line.
mnmd rootPrints 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 integration
mnmd ran in a shell that hasn't sourced monom. Run the installer, then open a new shell. Set MONOM_ACTIVE=1 to silence it in scripts.
monom: no project root found
No monom file here or in any parent, no git repository, and no pinned _MONOM_PROJECT_ROOT. cd into a project, or touch monom where 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, and discover.hide matches are skipped on purpose. Looking for deploy.sh? Tab lists it as deploy.
zsh: monom runs, but Tab does nothing
The source line comes before compinit in ~/.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.