The Context Switch That Kills Your Flow
You're debugging a production issue. You find the root cause — a missing null check in the order handler. You need to create a Linear issue before you forget the details.
So you open Linear. Click "New Issue." Pick the team. Type the title. Write the description. Set the priority. Add labels. Click "Create."
By the time you're done, 90 seconds have passed. You've lost the thread of what you were debugging. You switch back to the code. Where was that null check again?
What if you could just speak?
"The order handler crashes on null shipping address. Missing null check in process_order at line 142. P2 bug, affects about 5 percent of international orders."
Release. SpeechButton transcribes your words, transforms them into a structured Linear issue, and sends it through the GraphQL API. A new issue appears in your backlog:
ENG-847: Order handler crashes on null shipping address
Missing null check in process_order at line 142. Affects ~5% of international orders where shipping address is optional.
Priority: High | Labels: bug, orders
Three seconds. No browser. No clicking. No context switch. You're still in your editor, still looking at the code.
How It Works
Your voice ──▶ SpeechButton STT ──▶ Gemma 4 Local AI ──▶ Linear GraphQL API
(7ms) (Parakeet V3, (structures as (creates issue)
100% offline) title + desc +
priority, local)
Three components:
- SpeechButton captures and transcribes your voice locally on Apple Neural Engine
- Local AI (Gemma 4) reads your prompt file and extracts a structured issue (title, description, priority) from your transcription — entirely on your Mac
- A Python script in your
integrations/folder sends the JSON to Linear's GraphQL API
The result: a well-formatted Linear issue from your spoken description, in under 4 seconds total. Everything is local except the final Linear API call — no Anthropic API key needed.
Setup
Five steps. Under five minutes.
Step 1: Get your Linear API key
- Open Linear → Settings → Security & Access → API
- Click "Create new API key"
- Copy the key (starts with
lin_api_) - Add to your shell profile:
export LINEAR_API_KEY="lin_api_xxxxx"
Step 2: Choose your transform
SpeechButton supports two ways to structure your speech into a Linear issue. Option A is recommended — it's free, offline, and private.
Uses Gemma 4 running locally on your Mac. No API key required. No data leaves your machine except the final Linear issue.
transform = "prompts/linear_issue.md"
Uses Claude API for potentially more accurate structuring. Requires an ANTHROPIC_API_KEY and incurs per-request costs.
transform = "transforms/transform_claude.py prompts/linear_issue.md"
The rest of this guide uses Option A (local AI). If you choose Option B, set ANTHROPIC_API_KEY in your environment and swap the transform line in config.toml. The integration script and prompt file are identical for both options.
Step 3: SpeechButton config.toml
Add a hotkey for Linear. RightCommand creates an issue; the default left Command still pastes at cursor.
# ~/.config/speechbutton/config.toml [global] model = "parakeet-tdt-0.6b-v3-int8" language = "en" auto_punctuation = true [audio] vad_enabled = true vad_silence_threshold = 1.0 # Linear — create issue from voice [[hotkey]] key = "RightCommand" channel = "4" name = "linear" transform = "prompts/linear_issue.md" exec = "LINEAR_API_KEY=lin_api_xxx integrations/send_linear.py"
Step 4: Prompt file — spoken words → structured issue
Your raw speech is structured into a clean JSON payload for Linear. The transform field points to a prompt file that SpeechButton passes to the AI along with your transcription.
You say:
"The order handler crashes on null shipping address. Missing null check in process_order at line 142. P2 bug, affects about 5 percent of international orders."
You are a task formatter for Linear issue tracker.
Convert raw speech into a JSON object. Extract:
- title: concise issue title (under 80 chars)
- description: detailed markdown description
- priority: 1=urgent, 2=high, 3=medium, 4=low
Output ONLY valid JSON:
{"title": "...", "description": "...", "priority": 3}
What the transform produces:
{
"title": "Order handler crashes on null shipping address",
"description": "Missing null check in `process_order` at line 142.\n\nAffects ~5% of international orders where the shipping address field is optional.\n\n**Impact:** Crashes on order processing for international customers.",
"priority": 2
}
The AI infers priority from your words — "P2" becomes priority: 2, "urgent" becomes priority: 1, "minor" becomes priority: 4. Edit the prompt file any time to change the output format — no recompilation needed.
Step 5: Integration script — create the issue via Linear API
Takes the structured JSON from stdin and creates the issue via GraphQL. No external dependencies — uses only Python's standard library.
#!/usr/bin/env python3 """Create a Linear issue from stdin JSON.""" import json, os, sys, urllib.request API_URL = "https://api.linear.app/graphql" def graphql(key, query, variables=None): payload = {"query": query} if variables: payload["variables"] = variables req = urllib.request.Request(API_URL, json.dumps(payload).encode(), {"Content-Type": "application/json", "Authorization": key}) return json.loads(urllib.request.urlopen(req, timeout=15).read()).get("data", {}) def main(): text = sys.stdin.read().strip() if not text: sys.exit(0) # Strip markdown code fences if text.startswith("```"): lines = text.split("\n") text = "\n".join(lines[1:-1] if lines[-1].startswith("```") else lines[1:]) try: data = json.loads(text) except json.JSONDecodeError: data = {"title": text[:80], "description": text, "priority": 3} key = os.environ.get("LINEAR_API_KEY") if not key: print("LINEAR_API_KEY not set", file=sys.stderr) sys.exit(1) # Auto-detect first team teams = graphql(key, "query { teams { nodes { id name } } }") team_id = teams["teams"]["nodes"][0]["id"] # Create issue mutation = """mutation($input: IssueCreateInput!) { issueCreate(input: $input) { success issue { identifier title url } } }""" result = graphql(key, mutation, {"input": { "teamId": team_id, "title": data.get("title", "Untitled"), "description": data.get("description", ""), "priority": data.get("priority", 3), }}) issue = result["issueCreate"]["issue"] print(f"Created {issue['identifier']}: {issue['title'][:50]}") if __name__ == "__main__": main()
chmod +x ~/.config/speechbutton/integrations/send_linear.py
Done. Hold RightCommand, describe a bug, release. Linear issue created.
Real Workflows
During code review
You're reviewing a PR and spot three issues. Instead of writing inline comments that might get lost:
"The database query in get_users is doing N+1. Should batch with a join or use dataloader. Medium priority, performance label."
"The error handler on line 89 swallows the stack trace. Need to log the full error before returning the generic message. Low priority."
"Race condition in the cache invalidation. Two requests can read stale data between the delete and the rebuild. High priority, needs fix before merge."
Three Linear issues created in 30 seconds. Each with the right priority, right description, right labels. You never left your code review.
During standup
Your team is discussing yesterday's incidents. As items come up, you capture them in real-time:
"Follow up on the Redis timeout from yesterday. Connection pool might be exhausted under load. Need to add monitoring and possibly increase pool size. P3, assign to infra team."
No one has to remember to "create a ticket after standup." The ticket already exists.
During debugging
You're deep in a debugging session and keep finding related issues:
"While investigating the auth bug I found that the rate limiter is using a fixed window instead of sliding window. This means users can burst double the limit at window boundaries. P3, technical debt."
You found it, you logged it, you moved on. Zero interruption to your debugging flow.
Advanced: Multiple Teams, Auto-Assign
For teams with multiple Linear teams (Engineering, Design, DevOps), set up a channel per team:
# Engineering bugs [[hotkey]] key = "RightCommand" channel = "4" name = "linear-eng" transform = "prompts/linear_issue.md" exec = "LINEAR_API_KEY=lin_api_eng_xxx integrations/send_linear.py" # DevOps issues [[hotkey]] key = "RightOption" channel = "5" name = "linear-devops" transform = "prompts/linear_issue.md" exec = "LINEAR_API_KEY=lin_api_devops_xxx integrations/send_linear.py"
Same prompt file, different API keys scoped to each team. RightCommand for engineering bugs, RightOption for infra issues. The right team gets the right issues without you navigating team selectors.
You can also extend the transform to auto-assign based on keywords. Mention "database" and it assigns to your DBA. Mention "auth" and it routes to the security lead. The transform script is just code — make it as smart as you need.
Why Voice Beats Typing for Issue Creation
Linear issues created by typing tend to be either too terse ("fix auth bug") or too slow to write (two minutes of formatting). Voice hits the sweet spot:
"The order handler crashes on null shipping address, missing null check in process_order at line 142, P2, affects about 5 percent of international orders."
Order handler crashes on null shipping address
Missing null check in process_order at line 142. Affects ~5% of international orders.
Priority: High | Labels: bug, orders
The result is a better issue than you'd type manually, created in a fraction of the time. The AI transform handles the formatting you'd skip when rushing and the structure you'd over-think when not.
And because SpeechButton captures in 7ms, you can rapid-fire three issues in sequence without pausing between them. Hold, speak, release. Hold, speak, release. Hold, speak, release. Three issues in your backlog before you'd finish typing the first one.
Privacy
Here's exactly what stays on your Mac and what goes to the cloud:
| Component | Where it runs | Data sent externally |
|---|---|---|
| Voice capture | Your Mac | ✓ Nothing |
| Speech-to-text (Parakeet V3) | Apple Neural Engine | ✓ Nothing |
| AI transform (default) | Your Mac (Gemma 4, local) | ✓ Nothing |
| Linear API call | Your Mac | Structured issue → Linear's servers |
With the default local AI transform, everything stays on your Mac except the final Linear API call: voice → local STT (Parakeet V3, Apple Neural Engine) → local AI transform (Gemma 4, on your Mac) → Linear API. No audio leaves your machine. No transcription leaves your machine. Only the structured issue data reaches Linear's servers — which is where it needs to go anyway. No Anthropic API key required. If you prefer higher accuracy at the cost of privacy, you can optionally switch to the Claude API transform (Option B) — in that case, transcribed text is sent to Anthropic's servers for structuring.
Get Started
- 1 Download SpeechButton — free 15 minutes/day, no account needed
- 2 Get your Linear API key — Settings → API → Create key
-
3
Copy the config, prompt file, and integration script from this article into
~/.config/speechbutton/ -
4
Make the integration script executable:
chmod +x ~/.config/speechbutton/integrations/send_linear.py - 5 Hold RightCommand, describe an issue, release. Your first voice-created Linear issue in under a minute.
Start creating issues by voice today
Free 15 min/day · No account needed · macOS 15+ · Apple Silicon
Download for macOS — FreePro ($7.99/mo) removes the daily limit. Requires macOS 15+ and Apple Silicon.