nod

A human approval queue for AI agents, kept in a plain markdown file.
Agents ask. You nod (or don't) from your phone, your editor, or the terminal. The agent's blocked command picks up the answer and keeps going.
nod ask "Ship v2.3 to prod" --action "make deploy" --wait && make deploy

Why
Once you run more than a couple of agents, most of what they do is fine to just do. A few things are not: sending email as you, deploying prod, spending money, deleting data. The usual answer is that each agent pings you in its own chat. You lose track, the agent either stalls or guesses, and there's no record of what you said yes to.
nod gives every agent one place to ask and gives you one list to answer:
- One file, readable anywhere. The queue is
nod.md, a markdown checklist. Read it on your phone through Obsidian or Syncthing, git diff it, cat it over ssh.
- Safe with many writers. Every write takes an exclusive
flock and replaces the file atomically. Twenty agents can call nod ask at the same moment and get twenty distinct IDs. That's a test, not a promise.
- Agents block on your answer.
nod ask --wait exits 0 on approve and 1 on decline, so && does the gating. No SDK, no webhook server.
- Built for agents first. No prompts,
--json everywhere, stable exit codes, idempotency keys for retries, and real examples in every --help.
- MCP built in.
nod mcp serves the same queue to Claude Code, Cursor, or any MCP client.
- One static binary, no dependencies. Standard library Go only.
30-second quickstart
go install github.com/8thlyr/nod/cmd/nod@latest # or grab a binary from Releases
# an agent asks (prints the id, N-1)
nod ask "Send invoice to Acme" --action "send drafts/acme.eml" --agent mailer
# you look
nod digest
# 1 waiting on you
# N-1 mailer · Send invoice to Acme (2m)
# → send drafts/acme.eml
# you answer
nod approve N-1 --note "looks right"
The queue lives at ~/.local/share/nod/nod.md unless you say otherwise (see Where the queue lives).
Recipes
Gate a shell step. The agent blocks until you answer, then runs only on a yes:
nod ask "Rotate the Stripe key" --action "./rotate.sh stripe" --wait --timeout 2h && ./rotate.sh stripe
Exit codes from --wait and nod wait: 0 approved or done, 1 declined or dropped, 3 timed out (still open), 4 no such request.
Retry safely. Agents retry. Pass a key and asking twice returns the same request instead of queueing a duplicate:
id=$(nod ask "Merge PR 412" --action "gh pr merge 412" --key pr-412)
nod wait "$id"
Push new asks to your phone. nod watch runs a command on every change, with the details in env vars. Point it at ntfy, Slack, or anything with a URL:
nod watch --exec '[ "$NOD_EVENT" = asked ] && curl -s -d "$NOD_ID $NOD_SUMMARY → $NOD_ACTION" ntfy.sh/my-approvals'
Events are asked, approved, declined, done, dropped. Env vars: NOD_EVENT, NOD_ID, NOD_AGENT, NOD_SUMMARY, NOD_ACTION, NOD_REF, NOD_BY, NOD_NOTE.
A morning digest. nod digest fits in a push notification or a chat message (capped at about 1000 characters, oldest first):
nod digest --format ntfy | curl -s -d @- ntfy.sh/my-approvals # plain text
nod digest --format markdown # for chat or a daily note
Give an MCP agent the queue.
claude mcp add nod -- nod mcp
Or in any MCP client config:
{ "mcpServers": { "nod": { "command": "nod", "args": ["mcp"] } } }
The server exposes nod_ask, nod_status and nod_list, and its instructions tell the model not to act until the state is approved. There's no blocking wait tool on purpose: approvals can take hours, so agents poll nod_status. Requests are tagged with the client's name (for example claude-code) unless you pass --agent.
Answer in bulk.
nod list # what's open
nod approve all --dry-run # what would change
nod approve N-4 N-7 --note ok
nod decline N-5 --note "not today"
How it works
The whole queue is one markdown file:
# nod queue
Approvals waiting on a human. Edit with `nod`, read with anything.
## Open
- [ ] N-7 · deployer · Ship v2.3 to prod → ACTION: make deploy ENV=prod · REF: PR#412 · asked 2026-11-08 01:12
## Resolved
- [x] N-6 · mailer · Send invoice to Acme → ACTION: send drafts/acme.eml · REF: - · asked 2026-11-07 22:01 · APPROVED 2026-11-07 23:40 by jc (looks good)
- Writes. Each command takes an exclusive
flock on a sidecar .nod.md.lock, reads the file, changes it, writes a temp file, fsyncs it, and renames it over the original. Readers never see a half-written file, and concurrent writers queue up on the lock.
- IDs are
N-<n>, one more than the highest number anywhere in the file, so they never get reused.
- Your edits survive. Lines nod doesn't recognize (notes, headings, prose) are kept verbatim and in place. Tick a box by hand (
- [x]) and nod reads it as done. The parser is fuzz-tested for round-trip stability.
- Fields stay one line. Newlines collapse, and the separators
· and → inside a field are replaced, so a weird summary can't break the format.
- Waiting polls the file's mtime and size (1s by default,
--interval to change it). There's no daemon to run.
Where the queue lives
First match wins:
--file PATH
$NOD_FILE
./nod.md if it exists in the current directory (a per-project queue)
$XDG_DATA_HOME/nod/nod.md
~/.local/share/nod/nod.md
Other env vars: NOD_AGENT (who's asking, defaults to $USER) and NOD_USER (who's answering, defaults to $USER).
Commands
| Command |
What it does |
nod ask <summary> --action <cmd> |
Queue a request. --wait to block, --key for idempotency, --ref for context. |
nod wait <id> |
Block until resolved. Exit 0 yes, 1 no, 3 timeout. |
nod list |
Open requests (--all for history, --agent to filter). |
nod show <id> |
One request in full. |
nod approve / decline / done / drop <id...|all> |
Resolve. --note, --by, --dry-run. |
nod digest |
Short summary for a phone or chat. |
nod watch |
Print, or --exec a command, on every change. |
nod mcp |
MCP server on stdio. |
Every command takes --json and --file, and nod <command> --help shows examples. Exit codes: 0 ok, 1 declined (wait), 2 usage error, 3 timeout, 4 not found, 5 conflict (already resolved differently), 10 I/O error.
Non-goals
No server, no web UI, no auth. Access to the queue is access to the file, so use filesystem permissions, or sync it somewhere private. Cross-process locking needs flock, so Linux and macOS are supported. Windows builds only lock within one process.
License
MIT