Skip to content

AngusFu/askq

Repository files navigation

askq

A friendly native pop-up for AI coding agents to ask you a question.

When an agent needs a decision — "Which framework? Ship it or hold? Pick a migration strategy"askq pops a small native window with the choices, waits for you to answer, and hands the answer back. No more squinting at a numbered list buried in terminal scrollback: a real window you can answer from any pane, even when the terminal isn't focused (or isn't a terminal at all).

It's a tiny Wails (Go + the system WebView) desktop binary. Feed it questions as JSON, the user clicks, it prints the answers as JSON and exits — like code --wait, but for multiple-choice questions.

┌─────────────────────────────────────────┐
│  Which migration strategy?                │
│                                           │
│   1  ▸ Rewrite at once                    │
│   2  ▸ Run both in parallel   ← selected  │
│   3  ▸ Stay put                           │
│                                           │
│             [ Cancel ]   [ Submit ⌘↵ ]    │
└─────────────────────────────────────────┘

Quick start

Install the CLI globally (the right prebuilt binary for your platform is pulled in automatically — macOS arm64, Linux x64/arm64):

npm install -g askq-cli      # installs the `askq` command

No install needed for a one-off — npx -y askq-cli works too.

Pipe in a question, answer it in the window, read the answer from stdout:

echo '{"questions":[{"question":"Pick one","header":"Demo","multiSelect":false,
  "options":[{"label":"A","description":"first"},{"label":"B"}]}]}' | askq
# => {"questions":[...],"answers":{"Pick one":"A"}}

That's the whole contract: JSON in → a window → JSON out. Exit code 0 means answered, 1 means cancelled.

askq --help       # the full input/output schema (LLM-friendly)
askq version      # print the version

Five ways to use it, one binary

Surface What it is
CLI askq reads questions JSON (stdin or --file), prints answers.
MCP server askq mcp exposes an ask_user_question tool over stdio JSON-RPC — works with any MCP client (Claude Code, Cursor, Codex, Gemini, …).
Claude Code hook A PreToolUse hook intercepts the native AskUserQuestion and answers it through the window instead.
Skill A skill doc that teaches the agent when and how to ask.
Feishu card askq feishu-daemon sends interactive cards to a Feishu group chat. GUI and Feishu race: whichever answers first wins.

The last four are bundled in a Claude Code plugin (see Plugin below).


The contract

Input

A JSON object { "questions": [ … ] } on stdin or via --file <path>. A bare array [ {question…} ] is accepted too. Each question:

{
  "question": "Which framework?",   // shown to the user — also the KEY in answers (required)
  "header":   "Framework",          // short chip label (optional)
  "type":     "choice",             // "choice" (default) or "text" (a free-text box)
  "multiSelect": false,             // choice only: allow several picks
  "render":   "text",               // "markdown" renders text/descriptions/previews as sanitized Markdown
  "default":  "React",              // used on timeout (see --timeout)
  "options": [
    { "label": "React", "description": "", "preview": "optional monospace side-pane text" },
    { "label": "Vue",   "description": "" }
  ],
  // text-question extras:
  "placeholder": "Type here…",      // text only
  "multiline":   true               // text only, default true
}

Output

A single line of JSON on stdout, exit 0:

{ "questions": [ …echoed unchanged… ],
  "answers":   { "Which framework?": "React" } }
  • answers maps question text → chosen label.
  • Multi-select joins the picked labels with ", ""React, Vue".
  • "Other" (free text) → the answer is the string you typed.
  • type:"text" → the answer is the typed string.
  • Cancel (window closed / Esc / Cancel button) → exit 1, nothing on stdout.

Keep it to 1–4 focused questions; each label should be a distinct, mutually-exclusive choice.


Options & environment

--file <path>      read questions JSON from a file instead of stdin
--title <text>     banner title shown above the questions
--context <text>   banner subtitle, e.g. "project · session" (also the OS window title)
--recent <json>    JSON array [{"role","text"}] of recent turns, shown as a
                   scrollable conversation panel so you can tell windows apart
--timeout <secs>   auto-resolve after N seconds: the window shows a countdown and,
                   on expiry, answers each question from its "default" (exit 0,
                   output marked "timedOut") — or cancels if any lacks a default
Env var Effect
ASKQ_SOUND Sound played when the window opens (macOS). A path, or none/off to mute. Default: system Tink.
ASKQ_CASCADE Pixel step (e.g. 24) to fan out concurrent windows so they don't stack. Off by default — a lone window is centred.
FEISHU_ENABLED Set to 1 to enable Feishu card channel.
FEISHU_APP_ID Feishu app ID (from open.feishu.cn).
FEISHU_APP_SECRET Feishu app secret.
FEISHU_CHAT_ID Target group chat ID.
ASKQ_CHANNEL Channel selection: gui (default), feishu, all, or comma-joined like gui,feishu.

The plugin's hooks also read permission-window and timeout settings — see Configuration.

On macOS the window opens centred on the display under your cursor and takes keyboard focus, even though it's a plain binary with no .app bundle.


Use it in Claude Code

askq ships as a Claude Code plugin (hook + MCP server + skill). Install it from the marketplace — run these inside Claude Code:

/plugin marketplace add AngusFu/askq
/plugin install askq@askq

(The first command registers this repo as a marketplace; the second installs the askq plugin from it. Then /reload-plugins, or restart the session.)

Hacking on it locally? Load straight from a checkout instead:

claude --plugin-dir ./plugin

The plugin doesn't bundle a binary — a small launcher resolves askq in this order: $ASKQ_BIN (your local build) → a global askq on PATH → npx -y askq-cli.

As an MCP tool (any agent)

Register the server — same {command, args:["mcp"]} shape everywhere (.mcp.json for Claude Code, .cursor/mcp.json for Cursor, …):

{ "mcpServers": { "askq": { "type": "stdio",
  "command": "npx", "args": ["-y", "askq-cli", "mcp"] } } }

On a tools/call the server pops the window and returns the answers as the tool result.


Feishu Channel

askq supports sending interactive cards to a Feishu group chat as a parallel channel alongside the GUI window. The GUI and Feishu card race: whichever answers first wins.

How it works

  1. When FEISHU_ENABLED=1 and Feishu config is present, askq automatically starts a background daemon (askq feishu-daemon)
  2. The daemon maintains a WebSocket long connection to Feishu (using official Lark SDK)
  3. When a question is asked, askq sends an interactive card to the configured Feishu group
  4. The daemon listens for card button clicks and returns the answer
  5. If the GUI answers first, the Feishu session is cancelled (and vice versa)

Configuration

Set these environment variables in .claude/settings.json or .claude/settings.local.json:

{
  "env": {
    "FEISHU_ENABLED": "1",
    "FEISHU_APP_ID": "cli_xxx",
    "FEISHU_APP_SECRET": "xxx",
    "FEISHU_CHAT_ID": "oc_xxx",
    "ASKQ_CHANNEL": "all"
  }
}
Variable Required Description
FEISHU_ENABLED Yes Set to 1 to enable Feishu channel
FEISHU_APP_ID Yes Feishu app ID (from open.feishu.cn)
FEISHU_APP_SECRET Yes Feishu app secret
FEISHU_CHAT_ID Yes Target group chat ID
ASKQ_CHANNEL No Channel selection: gui (default), feishu, all, or comma-joined like gui,feishu

Channel selection

ASKQ_CHANNEL controls which channels are active:

  • gui — GUI window only (default)
  • feishu — Feishu card only
  • all — Both GUI and Feishu card (race mode)
  • gui,feishu — Same as all

Feishu app setup

Create an app on open.feishu.cn with:

  1. Bot capability enabled
  2. Event subscription: card.action.trigger (for button callbacks)
  3. Connection mode: WebSocket long connection
  4. Permissions: im:message:send_as_bot
  5. Bot added to the target group chat

Card layout

Cards are mobile-friendly: vertical button stacking, descriptions below buttons, no tables. Each option gets its own action block for easy tapping on mobile.

  • Single-select: One button per option, click to select
  • Multi-select: Checker components, check options then click Submit
  • Text input: Native input component with default value support

Manual daemon control

# Start daemon manually (usually auto-started)
./askq feishu-daemon &

# Check if daemon is running
ls -la /tmp/askq-feishu.sock

# Stop daemon
pkill -f "askq feishu-daemon"

Configuration (.claude/settings.json)

Full reference: docs/configuration.md — every ASKQ_* / FEISHU_* variable, defaults, hook mapping, and examples.

The plugin's hooks read their settings from environment variables, which you set in the env block of .claude/settings.json (project) or ~/.claude/settings.json (user) — the harness passes them through to every hook:

{
  "env": {
    // globs are COLON-separated (like $PATH), e.g. "a/**/*.md:b/*.md"
    "ASKQ_PERMISSION_TIMEOUT": "300"
  }
}

Permission gate

Variable Default What it does
ASKQ_PERMISSION 1 (on) Render Bash/Write/Edit permission prompts as an askq window instead of the in-terminal dialog. Set 0 to fall back to the native dialog.
ASKQ_PERMISSION_TIMEOUT (none) Auto-decision countdown in seconds (clamped ≤1500). Omit to wait indefinitely.

AskUserQuestion hook & banner

Variable Default What it does
ASKQ_HOOK_TIMEOUT (none) Forwarded as --timeout to the question window — mainly for headless claude -p batches (clamped ≤1500). Interactive sessions usually leave it unset (block until answered).
SB_RECENT_N 6 How many recent conversation turns the banner's scrollable panel shows.

Runtime knobs (any surface)

Variable Default What it does
ASKQ_BIN (resolve) Absolute path to an askq binary, taking priority over a global askq/npx — point it at a local build while developing.
ASKQ_SOUND system Tink Sound played when the window opens (macOS). A path, or none/off to mute.
ASKQ_CASCADE (off) Pixel step (e.g. 24) to fan out concurrent windows so they don't stack; off → a lone window is centred.

ASKQ_HITL_ACTIVE is set internally (so an edit the review editor makes can't re-trigger the hook) — don't set it yourself.


Build from source

You need Go 1.23+ and a C toolchain (Xcode CLT on macOS; on Linux libgtk-3-dev + libwebkit2gtk-4.1-dev). The frontend is plain HTML/JS embedded at compile time — no Node build step.

# Wails v2 needs the `production` build tag.
go build -tags production -o askq ./cmd/askq/

# smoke test
echo '{"questions":[{"question":"Pick one","header":"Demo","multiSelect":false,
  "options":[{"label":"A"},{"label":"B"}]}]}' | ./askq

For local plugin/MCP testing, scripts/build-plugin.sh builds + signs the binary and prints the ASKQ_BIN to export. On Linux, add the WebKit build tag: go build -tags "production webkit2_41" -o askq ./cmd/askq/

Releasing

scripts/bump-version.sh <X.Y.Z> syncs every manifest (the binary, plugin.json, and the npm packages) to one version. Then commit and tag:

scripts/bump-version.sh 0.2.1
git commit -am "release: v0.2.1"
git tag v0.2.1 && git push --tags

The tag triggers GitHub Actions, which builds the binary per platform, stamps the version from the tag, and publishes the npm packages via OIDC trusted publishing.


Platform support

Platform Status
macOS (arm64) Fully supported — centred window, keyboard focus, activation sound.
Linux (x64 / arm64) Builds & runs via WebKitGTK.
Windows Not built today (the Go/Wails code is portable; no release binary yet).

License

MIT

About

A GUI version `AskUserQuestion`

Resources

Contributing

Stars

Watchers

Forks

Packages

 
 
 

Contributors