Skip to content

Latest commit

 

History

2 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

human-intervention

A blocking dialog that lets an autonomous agent pull a human into the loop, and prints the human's answer to stdout.

An agent working through a long task will eventually hit something it cannot do itself: plug in a device, press a physical button, type a sudo password, confirm that a screen looks right. Ending its turn to ask is unreliable — the human is working on something else and may not look at the terminal for an hour, leaving the agent idle the whole time.

human-intervention turns that into a call the agent makes. It opens a native dialog that takes focus wherever the human happens to be, blocks until they answer, and prints their answer for the agent to read. The agent stays parked on one line of shell instead of guessing or giving up.

It works just as well as a graphical confirmation in an ordinary shell script — that's just not what it's for.

Built with Deno Desktop.

Requirements

The built app is self-contained — Deno Desktop bundles the runtime into the bundle, so running it needs nothing installed. Deno itself is only needed to build it.

macOS is required for the focus behaviour described below; the dialog itself is cross-platform.

Usage

human-intervention --text "Press the boot button on the device, then press Done" --confirm "Done"

The command does not return until the human answers. Whatever they choose lands on stdout.

Flags

Flag Description
--text Message to display (required)
--confirm Label for the confirm button; clicking it prints this value to stdout
--cancel Label for the cancel button; clicking it prints this value to stdout
--prompt Show a text input; the typed answer is printed on the second stdout line. Takes an optional placeholder
--no-sound Suppress the short alert sound the dialog plays when it opens
--plain Show --text as plain text instead of rendering it as markdown

If neither --confirm nor --cancel is given, the dialog shows only the message. Close the window to dismiss it.

Output

The chosen button label is printed to stdout. The process always exits with code 0 when the dialog completes — the outcome is in the output, not the exit code, so a caller reads stdout rather than branching on $?.

Action stdout
Click confirm button confirm label (e.g. Done), then the --prompt answer on the next line
Click cancel button cancel label (e.g. Cancel)
Decline with a reason declined, then the reason on the next line
Close the window dialog_closed

Errors (such as a missing --text) are printed to stderr with exit code 1.

Asking a yes/no question

Two buttons, and the label that was clicked comes back verbatim:

answer=$(human-intervention --text "Is the device connected?" --confirm "Yes" --cancel "No")
if [ "$answer" = "Yes" ]; then
  ...
fi

This is often cheaper than having an agent take a screenshot and reason about it — the human can see the answer at a glance.

Asking for a value

--prompt adds a focused text input below the message. The value it takes is used as the input's placeholder, and it may be omitted:

token=$(human-intervention --text "Paste the 2FA code" --prompt "6 digits" --confirm "OK" | tail -n +2)

Confirming prints the answer on the second stdout line, after the button label. Cancelling or closing the window discards it, so apart from a decline (see below) only a confirm produces a second line.

Declining with a reason

Every dialog carries a collapsed Decline with a reason disclosure above the buttons. Opening it reveals a text input and a Decline button; the button stays disabled until a reason is typed, since the reason is the whole point.

This is the escape hatch that makes the tool safe to hand to an agent. A cancel button only says no; a decline says no, because — and the reason is something the agent can act on. "The device is at the office, try again tomorrow" redirects the work instead of just halting it.

Declining prints declined on the first stdout line and the reason on the second, whatever the buttons are called and whether or not --prompt was passed. That makes it unambiguous for callers:

result=$(human-intervention --text "Ready to flash the firmware?" --confirm "Go" --cancel "Wait")
if [ "$(echo "$result" | head -n 1)" = "declined" ]; then
  reason=$(echo "$result" | tail -n +2)
  echo "human declined: $reason"
fi

Escape closes the disclosure while it is open, so it never answers the dialog halfway through typing. Enter inside the reason input declines.

Markdown

--text is rendered as markdown (headings, bold/italic, lists, links, code, blockquotes) by default, which suits agent-generated messages — they tend to arrive as markdown already:

human-intervention --text $'**Build failed**\n\n- missing dependency\n- see `npm install`' --confirm "OK"

Pass --plain to show the text verbatim instead:

human-intervention --plain --text "Ready for review" --confirm "OK"

Raw HTML in --text is escaped rather than rendered, so markdown mode can't be used to inject arbitrary HTML into the dialog.

Keyboard shortcuts

While the dialog is focused:

  • Enter — triggers the confirm button, if present; inside the decline reason input it declines instead
  • Escape — closes the decline disclosure if it is open; otherwise triggers the cancel button, if present, or closes the dialog

Using it from an agent

skills/human-intervention/ is an Agent Skill that teaches an agent when to reach for this command: what kinds of blockers warrant interrupting a human, which checks are cheaper to ask about than to screenshot, and the reminder that the call blocks — so any logging or process setup that needs to observe the human's action has to be running before the dialog opens.

Install it by symlinking it into your skills directory:

ln -s "$(pwd)/skills/human-intervention" ~/.claude/skills/human-intervention

Build

Needs Deno 2.9.0 or later, for the deno desktop subcommand.

deno task build

On macOS this produces dist/human-intervention.app, with the app icon taken from icons/icon.icns. Run it via:

./dist/human-intervention.app/Contents/MacOS/human-intervention --text "hello" --confirm "OK"

Install globally

The binary needs to be on your PATH for an agent to call it by name. From the repo root, symlink it into a directory that already is (the launcher resolves symlinks correctly):

ln -sf "$(pwd)/dist/human-intervention.app/Contents/MacOS/human-intervention" ~/.local/bin/human-intervention

Or add the launcher directory to your PATH in ~/.zshrc:

export PATH="/path/to/human-intervention/dist/human-intervention.app/Contents/MacOS:$PATH"

Reload your shell after changing your config:

source ~/.zshrc

App icon

icon.svg in the repo root is the source of truth. The rasterized icons live in icons/ and are committed, so a normal build needs no extra tooling:

File Platform
icons/icon.icns macOS
icons/icon.ico Windows
icons/icon.png Linux (1024px master)

The macOS variants follow Apple's icon grid: the artwork is scaled into an 824×824 box inside a 1024×1024 canvas, leaving a transparent margin so the icon sits at the same visual size as stock apps in the Dock. The Windows and Linux variants are full-bleed, which is the convention there.

They are wired up through desktop.app.icons in deno.json. After editing icon.svg, regenerate them:

deno task icons

That step needs librsvg (brew install librsvg) and, for the .ico, ImageMagick (brew install imagemagick).

Development

# Run tests
deno task test

# Run in dev mode (rebuilds on change)
deno task dev -- --text "hello" --confirm "OK" --cancel "Cancel"

How it works

human-intervention is a Deno Desktop app. It serves a minimal HTML dialog over a local HTTP server and opens it in a small frameless window. Button clicks are sent back to the Deno runtime, which prints the result and exits.

The bundle carries that runtime with it, as libruntime.dylib alongside the launcher — which is why the app is self-contained, and also why it weighs around 66 MB.

The build step patches the macOS launcher to suppress Deno Desktop startup noise so stdout stays clean for scripting — an agent parsing the output must not have to filter runtime chatter out of it.

The build passes --allow-net --allow-ffi: net for the dialog server, FFI for the AppKit calls in src/focus.ts. Note that deno desktop grants every permission when you pass no --allow-* flag at all, but restricts itself to exactly what you list as soon as you pass one. Dropping --allow-net therefore does not just disable the server — Deno.serve throws and the app exits before drawing anything, with no window and no error on stdout.

About

A skill + command line util for agents to have the user do something they can't

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages