unblock
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@unblockI need the production database password — file an ask and park."
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
unblock
One queue for everything your agents need from you.
An agent that hits a wall only a human can clear — an API key, a console click, an OAuth grant, an approval — registers a structured ask and either keeps working or parks. You answer a batch of them on one page, secrets included, and the parked ones wake up.
You never read a transcript to find out what an agent wanted.
Why it exists
Every human-in-the-loop tool for agents solves one third of this problem.
LangGraph's interrupt() has the right waiting semantics and ships no queue or
UI. HumanLayer and gotoHuman have structured requests and route them through a
SaaS. The local MCP question servers are single-agent, synchronous, and hand the
answer straight back through the tool result — which is exactly where an API key
must never go.
unblock is the three together: local-first, secrets that stay out of the model's context, and one queue across every agent on the machine.
Related MCP server: Clarify MCP Server
Product boundary
Unblock is a standalone local service. It owns one daemon, HTTP API, SQLite database, and answer UI. Agent integrations are clients of that service:
Hermes registers native
unblock_file,unblock_park,unblock_check, andunblock_canceltools plus the Unblock skill.The Herdr plugin is only a launcher and pane adapter for the same UI. Herdr is not a queue owner or synchronization peer.
Every client reaches the same database at
~/.local/state/unblock/queue.dbunlessUNBLOCK_STATE_DIRis set.
The installed Studio daemon polls live-doc approvals every 60 seconds, logs them in the scoping INDEX, and moves approved tabs to In flight (UNBLOCK_LIVEDOC_APPROVALS=0 disables it; UNBLOCK_LIVEDOC_POLL_MS sets the interval).
Day-old open asks go back to the filing lane once via lane-post; three-day-old asks move to the weekly decide-or-drop list and stop counting toward today. A recheck that fails three times is given up (recheck_unavailable_at). Configure the thresholds with UNBLOCK_RECHECK_AFTER_MS and UNBLOCK_WEEKLY_AFTER_MS, and the delivery executable with UNBLOCK_LANE_POST_BIN.
How it works
Two calls, and only one of them stops the agent.
call | blocks? | how many at once |
| no — returns a ticket, agent keeps working | unlimited |
| yes — holds until answered | one per agent |
That single constraint is what makes partial answering safe. Answering a filed ask never interrupts anything, so you can answer them in any order. Answering the parked one wakes the agent, and it collects every filed answer that landed while it was away.
An agent that needs three things declares one park with three fields. It does not park three times, because it can only be stopped in one place.
tool | does |
| register an ask and keep working |
| register an ask and wait on it |
| read what they have typed so far, without consuming it |
| revise an open ask in place — add, drop or reword questions |
| collect answers, and see what is part way filled in |
| withdraw an open ask |
Real blockers only
The queue costs a human attention, so the daemon only takes what only they can do. Every ask
carries only_you, the reason: credential (their sign-in or key), their_account (a click in
a console signed in as them), spend (money or a new account), message (a message to a real
person) or judgment (a product call). It also carries tried: one to eight lines saying what
the agent ran or attempted and why that could not clear it. A manual step needs a deep link to
the exact screen, not a home page. Titles and labels are checked for repo jargon. A second open
ask with the same project and title is refused with the existing ticket.
Each ask carries a level: P1 means a real person or money is waiting, or something is down; P2 holds up a lane or a build; P3 is a scope round or something shipped to review; P4 means decide when it suits you. If omitted, message and spend asks default to P1; blocker, consent, permission, or asks with dependent work default to P2; the rest default to P4. The queue sorts by level, then project order (Recruiter, Closer, Rails, Poker), then dependent work and age. Set UNBLOCK_PROJECT_ORDER to override the order, for example Poker=poker;Recruiter=recruiter,chord.
CLI
unblock [list] [--all] [--project P] what is waiting, grouped by project
unblock show <ticket> one ask in full (never secret values)
unblock answer <ticket> <value> answer a one-question ask in one line
unblock answer <ticket> name=value ... answer by question name
unblock close <ticket> <reason...> withdraw an open ask with a one-line reason
unblock file [path|-] file an ask from JSON
unblock update <ticket> [path|-] revise an open ask
unblock link <ticket> [--share] the stable queue link, or a 15-minute share link
unblock daemon start|stop|restart|status--json works on every command except reveal, ui and mcp. Exit codes: 0 ok, 1 daemon
unreachable, 2 usage, 3 no such ask, 4 rejected by the queue, 5 ask is not open.
Live asks
An ask is not a form you post and walk away from. Every keystroke drafts to the daemon, so an agent can watch someone think and ask the obvious follow-up while they are still on the page.
The loop is: file → watch the drafts → unblock_update → check.
unblock_file ub_7xk2m9 "which database?" + "which region?"
human picks "the replica"
unblock_peek ub_7xk2m9 draft.database = "replica"
unblock_update ub_7xk2m9 add_fields: [{ name: "lag_tolerance", ... }]
the open page grows a third question on its own
unblock_check ub_7xk2m9 all three answers at onceunblock_update mutates an OPEN ask through the same schema as unblock_file,
so a revision can never reach a shape a fresh ask could not. It keeps the
ticket, the link they already have open, and every draft on a field it does not
touch. Removing a field takes that field's draft and note with it. An answered,
bounced or cancelled ask is refused: a revision would change the question they
already answered.
Three ways to watch, cheapest first:
# one shot — draft_updated_at only moves when a human types
curl -s -H "Authorization: Bearer $(python3 -c 'import json,os;print(json.load(open(os.path.expanduser("~/.local/state/unblock/daemon.json")))["auth"])')" \
http://127.0.0.1:4488/api/asks/<ticket> | jq .draft_updated_at
unblock peek <ticket> # the same thing, readable
# a push stream: draft, updated, answered, sent_back, cancelled
curl -sN -H "Authorization: Bearer $UNBLOCK_AUTH" \
http://127.0.0.1:4488/api/asks/<ticket>/eventsGET /api/asks/:ticket returns the ask with draft, draft_reply,
field_context, draft_updated_at, updated_at and status. Like everything
under /api it is loopback-and-tailnet only, and it carries the daemon secret
from ~/.local/state/unblock/daemon.json.
A draft is not an answer. They are mid-thought, they can still change it, and they have not pressed the button — read it to decide what to ASK next, never to act on as though it were decided.
Secrets
A secret typed into the form never travels back through the channel that lands in a model's context. The daemon stores it and hands the agent a reference:
op://Private/abc123/credential # 1Password — masks the value if printed
ub_9cjp4t-stripe_key # macOS keychain
$UNBLOCK_STRIPE_KEY # env file, 0600The agent resolves it at the point of need and never prints it. 1Password is
preferred when op is signed in, because op run masks the value even if a
subprocess echoes it — the only backend with that protection.
Install
npm install -g unblockd # daemon, MCP server, CLI (binary: unblock)
unblock daemon startThen point an agent at it. For Claude Code:
claude mcp add unblock -- npx unblockd mcpAnd install the skill so agents know when to park:
npx skills add aneym/unblock --skill unblock -gherdr
herdr plugin install aneym/unblock --yesClaude Code in herdr panes
Add these hooks to ~/.claude/settings.json (replace /path/to/unblock with your installed package path):
{
"hooks": {
"PreToolUse": [{ "matcher": "AskUserQuestion", "hooks": [{ "type": "command", "command": "node /path/to/unblock/hooks/claude-ask.js" }] }],
"PermissionRequest": [{ "hooks": [{ "type": "command", "command": "node /path/to/unblock/hooks/claude-permission.js" }] }]
}
}In herdr panes, questions go to unblock; permission prompts stay visible until you answer. Set UNBLOCK_ALLOW_DIALOG=1 to use Claude's dialogs instead. Outside herdr panes the hooks do nothing.
Hermes
From a canonical checkout, install the live source into one Hermes profile:
HERMES_HOME=~/.hermes/profiles/bot bin/hermes-install.shThe installer symlinks both the native plugin and skill to this checkout, so a pull in the canonical Unblock repo updates Hermes without creating a second source tree. It then enables the plugin and runs Hermes' real plugin doctor. Restart the active Hermes CLI/TUI/gateway process, or start a fresh session, to load newly registered tools.
For a normal cloned plugin install instead of a live checkout, use
hermes plugins install aneym/unblock --enable; synchronize it explicitly with
hermes plugins update unblock.
herdr:// deeplinks (macOS)
Each ask card links its origin pane (pane w4D:p8) as a herdr:// URL, so a
click in the browser jumps straight to the pane that asked. Install the scheme
handler once:
bin/herdr-deeplink-install.shThat builds ~/Applications/Herdr Link.app (registered for herdr://) and
installs ~/.local/bin/herdr-deeplink, which runs herdr agent focus on the
pane — falling back to the tab, then the workspace, when the pane is gone —
and brings the Herdr terminal forward.
alt+p u opens unblock mode: a zoomed pane with the whole queue, scoped to the
active profile, answerable in place. Answers wake the agent in its pane.
The plugin also lists agents herdr has detected as blocked but which never declared an ask — below the declared ones, so a silent stall is still visible.
Answering from a phone
unblock link mints an ephemeral URL. It dies when you submit and expires on a
timer, so a stale tab in your pocket is not still live tomorrow. Serve it over
your own tailnet, or use a quick tunnel if you have no tailnet:
tailscale serve --https=8799 127.0.0.1:4488 # tailnet only
cloudflared tunnel --url http://127.0.0.1:4488 # no accountOne stable tailnet URL
Behind tailscale serve the daemon can trust Tailscale's identity headers and
serve the queue at one bookmarkable address, no token in the URL. Put the
settings in ~/.config/unblock/config.json so every spawner (the herdr startup
hook, an MCP server's auto-start, the CLI, launchd) starts the same daemon:
{
"public_origin": "https://studio.tailf266ac.ts.net:8797",
"trusted_proxy": "tailscale",
"allowed_users": ["you@example.com"],
"root": "/path/to/this/checkout"
}public_origin is exactly one https URL; wildcards, paths, and plaintext off
loopback are rejected. Requests whose Host is neither loopback nor that origin
get 403 before authentication runs. trusted_proxy only ever means Tailscale,
and only for requests that arrived on the public origin carrying a
tailscale-user-login in allowed_users. root names the checkout the daemon
must run from, so a second copy of this repo (a herdr-managed clone, say)
never wins the port with a stale panel. Environment variables of the same
names (UNBLOCK_PUBLIC_ORIGIN, UNBLOCK_TRUSTED_PROXY, UNBLOCK_ALLOWED_USERS,
UNBLOCK_PORT, UNBLOCK_ROOT) override the file. GET /api/health reports
public_origin, trusted_proxy, and which keys the file supplied.
Agents then hand out https://<origin>/#ask=<ticket>; for someone off the
tailnet, POST /api/links {ticket} still mints a burn-on-answer token.
Scoping pages
A scoping page is one document with threads anchored to quotes. Open threads
are questions or comments; resolved threads hold decisions. The page lives at
<public_origin>/s/<slug>, with state under UNBLOCK_SCOPING_DIR
(default: ~/.agent-rails/scoping). Only the daemon writes scope.json.
Lanes use the CLI instead of editing that file. A No keeps a question open; a
new option uses scope reply <slug> T# --rec "option" [--why "reason"] "reply".
unblock scope ask demo --section plan --quote "phones first" --rec "Phones first" --option "Phones first" --option "Desktop first" "Which screen ships first?"
unblock scope react demo T4 # acknowledge; --clear removes it
unblock scope reply demo T4 "Moved voice to round two."
unblock scope doc demo --from plan.md
unblock scope patch demo plan --from section.md
unblock scope edit demo T4 --section plan --quote "We build for phones first" --option "Phones first" --option "Desktop first"
unblock scope resolve demo T4
unblock scope threads demo --openscope resolve on Alex's comment only records your answer; his comment stays open until Alex resolves it.
unblock scope app <slug> recruiter|closer|rails-admin files a scope under its app in Rails Admin.
Approve scope on the page with an optional final note; the lane receives APPROVED and the note, then moves to build (with changes: fold the note into the doc first).
Use a fence with opening line ```demo, then src: demo.html (or an https URL) and closing line ```; optional keys are height, frame and allow, and local src files are uploaded.
Use ```video with src: recording.mp4 and optional poster: cover.png, then ```; local src/poster files are uploaded, and a following Figure: caption anchors threads.
A markdown doc starts with # Title; ## Heading {#stable-id} starts a section.
JSON input is a sections array or {sections}. Doc rewrites keep all threads;
threads whose quotes disappeared are reported as detached. Each rewrite saves
a revision. scope doc <slug> exports markdown; --json gives revision and sections.
scope edit <slug> T# [--section id --quote "text"] [--option "text" ...] moves a thread to
its new sentence and/or sets its options without adding a message, changing its status or notifying the pane.
Only lanes can edit threads. Questions accept 2–5 options of 1–200 characters, with the
current recommendation first. Repeat --option on ask, edit or a reply --rec;
a new recommendation without options removes the old list. The API also accepts an
empty options array to remove it. scope threads lists options after the recommendation.
Existing v1 files are read as v2 and backed up on their first write.
A trusted tailnet viewer can select text to comment, reply or resolve a thread.
Their actions reach the lane's pane; Not now parks a thread. Tags such as
@pHS or @another-scope also route the comment to that lane. After a decision, the lane rewrites the doc
and confirms it with scope resolve. A reply without T# is a general comment.
scope list, scope url <slug> and scope notes <slug> [--since N] remain;
all scoping commands support --json. Voice uses the same thread paths and the
queue's voice keys, spend ledger and $20 monthly cap.
Unslop gate
Lane-written headings, body text, captions and thread text are checked for AI tells
and internal jargon. Code, images and URLs are skipped; human and Admin relay
words are never checked. Findings refuse the write with HTTP 422 and CLI exit 2.
Run /unslop, then unblock scope lint <slug> --from doc.md to check locally
without publishing. Repeat --keep "Name" to keep a real name on lint or a write.
Sections over 120 prose words get a warning, not a refusal.
Doc writes and local lint check only new or changed sections; if lint cannot read
the current scope, it checks every section and says so.
Scope images and mocks
Put each image on its own line:  or
. unblock scope doc <slug> --from doc.md
uploads local images and inlines and renders HTML mocks in light and dark themes.
Mocks get a viewport tag if they lack one, and "phone" renders at 390 CSS px (780 px image; desktop at 1280 CSS px). Adjacent image lines share a
following Figure: <caption> line. Questions anchor to that caption, not the image's
alt text. Exports keep the stored asset: references, so re-importing needs no render.
POST /api/scope/<slug>/threads/<T>/pick {text} is a human-only, read-only ask picker; UNBLOCK_ASK_PICKER_BIN selects its binary and UNBLOCK_ASK_PICK_MIN sets the confidence cut (default 0.6).
Rails Admin relay
The unblock-admin-relay agent-secret handle (override with
admin_relay_key_ref / UNBLOCK_ADMIN_RELAY_KEY_REF, or set
UNBLOCK_ADMIN_RELAY_TOKEN) enables a scope-only relay when the token has at
least 32 characters. Send it in X-Unblock-Relay on loopback hosts only.
The relay may read GET /api/scope, GET /api/scope/<slug> and
GET /api/scope/<slug>/assets/<id>, create human
comments with POST /api/scope/<slug>/threads, and use the four thread write
verbs reply, resolve, reject, and park. Each write requires a unique
client_id (1–64 letters, digits, underscores or hyphens) so retries do not
land twice. Writes are marked via: admin; that field may be omitted or set
to admin, never another channel. No other daemon route accepts this credential.
Layout
src/schema.js ask + field validation, the profile rule
src/store.js SQLite queue, one-park-per-agent, drafts, links
src/secrets.js 1Password -> keychain -> env file
src/daemon.js HTTP API, the answer page, SSE (queue-wide and per ask)
src/mcp.js MCP server: file / park / peek / update / check / cancel
web/ the answer page
plugin/ thin herdr launcher / pane adapter
hermes.py native Hermes client for the standalone API
plugin.yaml Hermes plugin manifest
skills/unblock/ the discipline agents follow before parkingZero npm dependencies. The queue is one SQLite file at
~/.local/state/unblock/queue.db.
License
MIT
This server cannot be deployed
Maintenance
Related MCP Connectors
Give any AI agent a way to ask a person — for approval, a decision, an answer or a review.
Shared task queue for humans and AI agents: leases, handoffs, approvals and signed receipts.
Get a real human to verify, decide, or improve an AI agent's work.
Agent-to-agent network for teams: dm, who-knows-X routing, shared rooms. Human-in-the-loop.
Related MCP Servers
AlicenseNot gradedqualityDmaintenanceEnables AI agents to submit tasks for human or AI review and receive decisions via MCP tools, adding human review checkpoints to workflows.MIT- AlicenseNot gradedqualityDmaintenanceEnables AI agents to ask clarification questions and receive structured user input through a Human-in-the-Loop interface.1MIT
- AlicenseBqualityDmaintenanceEnables AI agents to ask human operators questions through polished browser dialogs, supporting text/choice/confirmation inputs and file attachments.7MIT
- FlicenseNot gradedqualityDmaintenanceEnables AI agents to request human decisions for subjective or high-stakes choices through MCP tools like ask_human and provision_api_key.-