Building granola-cli: AI Meeting Notes in Your Terminal
Disclaimer: This project is an independent open-source tool and is not affiliated with, endorsed by, or connected to Granola.ai.
What if you could query your meeting history like a database? granola-cli brings your Granola.ai meeting notes to the command line—grep transcripts, export to JSON, and pipe action items directly into Claude Code or your task manager.
What is Granola?
Granola is an AI meeting assistant that sits in your menu bar. It records system audio, transcribes locally, and uses LLMs to produce structured summaries—key decisions, action items, discussion points. No bots joining your calls, no awkward "Granola is recording" notifications to your teammates. At Buffer, it has become a daily driver for our team.
The notes it generates are excellent. But I hit a ceiling.
The Problem I Needed to Solve
I wanted raw data in my terminal. I wanted to feed last week's engineering syncs into Claude Code to plan my next sprint. I wanted to grep through transcripts or pipe action items directly into my task manager.
Copy-pasting from a web UI fell short. I found Joseph Thacker's research on reverse engineering the Granola API, plus the getprobo/reverse-engineering-granola-api repo. The groundwork existed. A proper CLI for daily use did not.
So I built one.
How I Mapped Granola's API
The Granola desktop app stores authentication tokens in a local JSON file. The CLI reads these credentials, stores them securely in your system keychain via cross-keychain (which I wrote about previously), and uses them to call Granola's internal APIs.
Key endpoints I mapped:
POST /v2/get-documents— list meetings with cursor paginationPOST /v1/get-document-metadata— notes and participant dataPOST /v1/get-document-transcript— transcript segments with speaker detectionPOST /v2/get-document-lists— folders and workspace organization
Query, Filter, and Export Your Meetings
List and Filter
# Recent meetingsgranola meeting list --limit 10# Filter by date (natural language supported)granola meeting list --date yesterdaygranola meeting list --since "last week"granola meeting list --since 2025-12-01 --until 2025-12-15# Filter by attendee or searchgranola meeting list --attendee "[email protected]"granola meeting list --search "quarterly planning"
View Content
# Full meeting details with participantsgranola meeting view <id># Your handwritten notes (converted from ProseMirror to Markdown)granola meeting notes <id># AI-generated summary with key decisions and action itemsgranola meeting enhanced <id># Full transcript with speaker detectiongranola meeting transcript <id>granola meeting transcript <id> --timestamps
Export for Pipelines
# Export everything about a meetinggranola meeting export <id> --format jsongranola meeting export <id> --format toon# Pipe to LLMsgranola meeting enhanced <id> --output toon | llm "What were the action items?"
40% Fewer Tokens with TOON
The CLI supports TOON (Token-Oriented Object Notation), a format designed for LLM consumption. TOON delivers the same structured data as JSON using 40% fewer tokens.
When you pipe meeting data to Claude or another model, every token saved means more context for your question. TOON determines whether one meeting or three fits your context window.
$ granola meeting export abc123 --format json | wc -c15234$ granola meeting export abc123 --format toon | wc -c9140
Same data. 40% smaller. That's the difference between asking a follow-up question or hitting your context limit.
Turning Meeting History into AI Context
This is why I built it: to empower my AI agents.
I use Claude Code heavily. With granola-cli installed, I can ask Claude to analyze my meetings directly:
You: Check my engineering syncs from last week and list any blockers mentioned.Claude: I'll query your recent meetings using granola-cli.[Runs: granola meeting list --since "last week" --search "sync"]Found 3 engineering syncs. Analyzing transcripts...Blockers mentioned:1. CI pipeline flakiness blocking the release (Dec 18 sync)2. Waiting on design review for the dashboard redesign (Dec 19 sync)3. API rate limiting issues with the third-party integration (Dec 20 sync)
No copy-pasting. No switching windows. Claude reads the data directly and gives me answers.
I've also built Agent Skills that check meeting history, summarize decisions, and surface blockers from previous syncs. Your meeting history becomes a queryable database for your AI workflow.
Under the Hood
Secure Credential Storage
I refuse to store API tokens in plaintext config files. Too many CLI tools dump
secrets into ~/.config/toolname/credentials.json. One accidental git add .
or misconfigured backup exposes your tokens.
The CLI stores credentials via cross-keychain in your OS's native credential manager—macOS Keychain, Windows Credential Manager, or Linux Secret Service. These systems encrypt secrets at rest, integrate with your login session, and follow platform security best practices. Your Granola tokens never touch the filesystem in readable form.
Token Refresh with File Locking
Granola uses single-use refresh tokens—each token works once before invalidation. This improves security but creates a race condition: if two CLI processes refresh simultaneously, one gets a valid token while the other wastes the refresh token and fails.
The CLI solves this with file-based locking. Before refreshing, the process acquires an exclusive lock on a temp file. If another process is already refreshing, the second waits (30-second timeout) rather than racing. The lock releases immediately after refresh completes, so parallel CLI invocations work smoothly—they take turns when needed.
ProseMirror to Markdown Conversion
Granola stores notes in ProseMirror format—the same rich-text framework Notion, The New York Times, and Atlassian use. It represents content as a JSON tree of nodes with marks (formatting) attached.
The CLI walks this tree, converting it to Markdown. Headings become # lines,
lists become - items, text marks become their Markdown equivalents: bold wraps
in **, italic in *, code in backticks. The conversion preserves nested
structures, so a bulleted list inside a blockquote renders correctly. The
result: readable Markdown you can pipe to other tools, search with grep, or feed
to an LLM.
Natural Language Date Parsing
Nobody types ISO dates willingly. The CLI accepts "today", "yesterday", "3 days
ago", "last week", or partial dates like "Dec 1". For ranges, combine --since
and --until with any format. The parser handles the rest.
The parser normalizes input, handles edge cases (what does "last week" mean on a Monday?), and returns UTC timestamps matching Granola's API expectations. The common case—"show me yesterday's meetings"—becomes a single intuitive flag.
4 Hours with Claude Code Opus 4.5
I built this tool in about 4 to 5 hours pairing with Claude Code Opus 4.5. I focused on architecture and intent while Claude handled implementation. The result: a production-ready CLI with strict TypeScript, 95%+ test coverage across 630+ test cases, and modular design—all in a single afternoon.
This is "vibe engineering" in practice. I skipped the lengthy planning phase, described what I wanted, reviewed the output, and iterated quickly.
Get Started
# Installnpm install -g granola-cli# Login (reads credentials from your Granola desktop app)granola auth login# List your meetingsgranola meeting list
The source is at github.com/magarcia/granola-cli. Issues and PRs welcome.
Related posts:
- Cross-Platform Secret Storage in Node.js with cross-keychain - The library granola-cli uses for secure credential storage
- Asking AI to Build the Tool Instead of Doing the Task - How I approach AI-assisted development