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 ⌘↵ ] │
└─────────────────────────────────────────┘
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` commandNo install needed for a one-off —
npx -y askq-cliworks 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| 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).
A JSON object { "questions": [ … ] } on stdin or via --file <path>. A bare
array [ {question…} ] is accepted too. Each question:
A single line of JSON on stdout, exit 0:
{ "questions": [ …echoed unchanged… ],
"answers": { "Which framework?": "React" } }answersmaps 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.
--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.
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 ./pluginThe 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.
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.
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.
- When
FEISHU_ENABLED=1and Feishu config is present, askq automatically starts a background daemon (askq feishu-daemon) - The daemon maintains a WebSocket long connection to Feishu (using official Lark SDK)
- When a question is asked, askq sends an interactive card to the configured Feishu group
- The daemon listens for card button clicks and returns the answer
- If the GUI answers first, the Feishu session is cancelled (and vice versa)
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 |
ASKQ_CHANNEL controls which channels are active:
gui— GUI window only (default)feishu— Feishu card onlyall— Both GUI and Feishu card (race mode)gui,feishu— Same asall
Create an app on open.feishu.cn with:
- Bot capability enabled
- Event subscription:
card.action.trigger(for button callbacks) - Connection mode: WebSocket long connection
- Permissions:
im:message:send_as_bot - Bot added to the target group chat
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
# 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"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"
}
}| 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. |
| 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. |
| 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_ACTIVEis set internally (so an edit the review editor makes can't re-trigger the hook) — don't set it yourself.
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"}]}]}' | ./askqFor 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/
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 --tagsThe 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 | 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). |
MIT
{ "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 }