AI-powered audio recording, transcription and post-processing β transcribe with OpenAI Whisper, Gemini, ElevenLabs Scribe or local faster-whisper, then summarize with OpenAI, Gemini, or your Claude Code / Codex / Hermes Agent login.
# Install
git clone https://github.com/fabienpiette/goscribe && cd goscribe
make build && make install
# Store your API key once
goscribe -set-key YOUR_OPENAI_API_KEY
# Transcribe
goscribe meeting.mp3
# Transcribe + summarize in one go
goscribe -action openai-meeting-summary meeting.mp3- Dual providers β OpenAI (Whisper + GPT) and Google Gemini, with automatic fallback between them
- Local agent harnesses β run post-processing actions through your Claude Code, Codex or Hermes Agent CLI login instead of an API key
- Search β
goscribe search tokens person:alex -since 2026-09-01finds meetings by their tags - Live assistant β
goscribe assistfollows a meeting live (ElevenLabs realtime) and, in your terminal only, suggests questions, answers from your notes, missed points and risks, while keeping a running summary - Recording β
goscribe recordcaptures the mic and/or computer sound to mp3 (Ctrl+C to stop), lets you mute either side mid-recording, warns about silent sources, and can transcribe right after - Searchable tags β
-tagswrites a<name>-tags.yml(type, summary, topics, people, tickets, projects) next to the transcript, reusing existing tags, sogrepfinds every meeting on a subject - Pluggable transcription β transcribe locally and offline (faster-whisper) or with ElevenLabs Scribe (speaker labels), then post-process with OpenAI or Gemini
- 18 built-in actions β meeting summaries, action items, technical notes, retrospectives, and more
- Auto-select mode β let the AI pick the most relevant actions for your transcript
- Large file handling β audio splitting and transcript chunking happen transparently
- HTTP API + async job queue β Redis-backed server mode for homelab and self-hosted deployments
- Custom actions β define your own post-processing prompts in YAML config
Prerequisites: Go 1.24+. Depending on the features you use:
- ffmpeg β for
goscribe recordandgoscribe assist, and to split files over the provider size limits - pactl (PipeWire or PulseAudio) β for
goscribe recordandgoscribe assist - whisper-ctranslate2 β for
-transcriber local(uv tool install whisper-ctranslate2) - claude, codex or hermes CLI, logged in β for
-provider claude/-provider codex/-provider hermes
git clone https://github.com/fabienpiette/goscribe
cd goscribe
make build && make installcp .env.example .env
# Add your API key(s) to .env, then:
make docker-upThe server starts on port 8080 with Redis managed by Docker Compose. See Server Mode below.
# Use Gemini instead of OpenAI
goscribe -set-gemini-key YOUR_GEMINI_KEY
goscribe -provider gemini meeting.mp3
# Transcribe locally (offline), summarize with OpenAI
goscribe -transcriber local -action openai-meeting-summary meeting.mp3
# Transcribe with ElevenLabs Scribe (speaker labels)
ELEVENLABS_API_KEY=... goscribe -transcriber elevenlabs meeting.mp3
# Run actions through your local Claude Code (or Codex) login
goscribe -transcriber local -provider claude -action openai-meeting-summary meeting.mp3
# Tag a meeting for search (new audio, or an existing transcript)
goscribe -provider codex -tags meeting.mp3
goscribe -provider codex -transcript meeting-transcript.txt -tags
grep -rlw dark-mode --include='*-tags.yml' ~/records
# Run multiple actions at once
goscribe -action openai-meeting-summary,openai-action-items meeting.mp3
# Let AI pick the best actions
goscribe --auto meeting.mp3
# Process an existing transcript
goscribe -transcript meeting-transcript.txt -action openai-action-items
# Process multiple transcripts together
goscribe -transcript call1.txt call2.txt -action openai-meeting-summaryOutput files are written alongside the input:
<filename>-transcript.txt Raw transcription
<filename>-<action-id>.txt Post-processed output
<filename>-tags.yml Tags for search (with -tags)
All flags:
| Flag | Description |
|---|---|
-k |
OpenAI API key |
-gemini-key |
Gemini API key |
-provider |
Actions engine: openai (default), gemini, claude, codex or hermes |
-transcriber |
Transcription engine: local, elevenlabs, openai or gemini (default: config, else same as -provider) |
-elevenlabs-key |
ElevenLabs API key (default: config, then $ELEVENLABS_API_KEY) |
-keyterms |
Comma-separated names/jargon for ElevenLabs to favor (20% surcharge) |
-num-speakers |
Max speakers for ElevenLabs diarization (default: auto) |
-whisper-model |
Whisper model for -transcriber local (default: small) |
-language |
Audio language code for local and elevenlabs, e.g. fr (default: auto-detect) |
-no-fallback |
Disable automatic provider fallback |
-action |
Action ID(s), comma-separated |
--auto |
AI picks the best actions |
-transcript |
Process existing transcript file(s) |
-tags |
Write <name>-tags.yml next to the transcript (works with -transcript alone) |
-tags-root |
Folder whose tag files seed the shared vocabulary (default: tags_root in config, else the transcript folder's parent) |
-o |
Transcript file name (default: <audio name>-transcript.txt next to the audio) |
-config |
Custom config file path |
-list-actions |
Show all available actions |
-set-key |
Save OpenAI key to config |
-set-gemini-key |
Save Gemini key to config |
-set-provider |
Save default provider to config |
-set-transcriber |
Save default transcription engine to config |
-init |
Reset config to defaults |
The server exposes a REST API backed by an async job queue. Jobs are processed by a worker and results are stored in Redis until retrieved.
# Submit a transcription job
curl -X POST http://localhost:8080/jobs \
-F "file=@meeting.mp3" \
-F "actions=openai-meeting-summary"
# Poll for results
curl http://localhost:8080/jobs/<job-id>
# List available actions
curl http://localhost:8080/actions| Endpoint | Description |
|---|---|
POST /jobs |
Submit an audio file or transcript for processing |
GET /jobs/{id} |
Fetch job status and results |
GET /actions |
List configured post-processing actions |
GET /health |
Health check |
GET /docs |
Swagger UI |
GET /openapi.yaml |
OpenAPI 3.0 spec |
POST /jobs accepts: file (audio), transcript (text), actions
(comma-separated IDs or auto), provider, webhook_url (called with
results on completion).
Scaling: set MODE=api and MODE=worker in separate containers using
docker compose --profile split up. Both containers must share the same Redis
instance and uploads volume.
Config lives at ~/.goscribe/config.yml. Add custom actions:
provider: "openai" # openai, gemini, claude, codex or hermes
openai_api_key: ""
gemini_api_key: ""
gemini_model: "gemini-2.0-flash"
# Optional models for the local agent CLIs (empty: the CLI's default)
claude_model: ""
codex_model: ""
hermes_model: "" # empty: the model section of your Hermes config
hermes_provider: ""
# Optional transcription engine settings
transcriber: "local" # local, elevenlabs, openai or gemini
whisper_model: "small" # local only; e.g. large-v3-turbo for accuracy
elevenlabs_api_key: ""
elevenlabs_model: "scribe_v2"
elevenlabs_keyterms: ["Acme"] # names/jargon to favor (20% surcharge)
elevenlabs_num_speakers: 0 # max speakers, 0 = auto
elevenlabs_verbatim: false # true keeps filler words
language: "" # e.g. "fr"; empty = auto-detect
# Optional tags, search and record defaults
tags_root: "" # folder holding your recordings, for -tags and search
record_source: "both" # mic, system or both
record_mic: "" # device name from `goscribe record -list-sources`
record_system: "" # computer sound monitor; empty = default output
record_quality: "voice" # voice or music
# Optional assist settings (and a librarian: block, see "Live assistant")
assist_recap: 5m # how far back the w recap goes
assist_local_model: "base" # faster-whisper model when ElevenLabs is out of quota
post_actions:
- id: "custom-summary"
name: "Custom Summary"
description: "My custom summary"
type: "openai" # or "gemini"
prompt: |
Summarize focusing on key decisions and next steps.
model: "gpt-3.5-turbo"
temperature: 0.3
max_tokens: 1500Reset to defaults: goscribe -init
All configuration is via environment variables. Copy .env.example to .env:
| Variable | Default | Description |
|---|---|---|
MODE |
all |
all | api | worker |
PORT |
8080 |
HTTP listen port |
REDIS_URL |
redis://redis:6379 |
Redis connection URL |
OPENAI_API_KEY |
β | OpenAI API key |
GEMINI_API_KEY |
β | Gemini API key |
GOSCRIBE_PROVIDER |
openai |
Default provider |
MAX_UPLOAD_MB |
100 |
Max audio upload size |
RESULT_TTL_HOURS |
24 |
Job result retention in Redis |
GOSCRIBE_CONFIG_FILE |
β | Path to custom config.yml for extra actions |
GEMINI_MODEL |
gemini-2.0-flash |
Gemini model |
UPLOADS_DIR |
/tmp/goscribe-uploads |
Where uploaded audio waits for the worker |
SHUTDOWN_TIMEOUT_SECONDS |
30 |
Graceful shutdown drain timeout |
18 actions ship by default. Run goscribe -list-actions to see them all.
| Category | Actions |
|---|---|
| Meetings | openai-meeting-summary openai-action-items openai-standup openai-one-on-one openai-client-meeting |
| Technical | openai-tech-meeting openai-decision-record openai-retrospective openai-incident-postmortem |
| Business | openai-executive-brief openai-project-kickoff openai-key-insights |
| Learning | openai-training-session openai-interview-notes openai-brainstorm openai-qa-format |
| HR | openai-hr-meeting openai-company-webinar |
OpenAI (default) β Whisper for transcription, GPT for post-processing. 25 MB file limit. Get an API key.
Gemini β native audio understanding for transcription and processing. 20 MB inline limit, up to 1M token context. Get an API key.
When both keys are configured, goscribe automatically falls back to the other provider on failure. Disable with -no-fallback.
claude, codex and hermes run post-processing actions and --auto through the Claude Code, Codex or Hermes Agent CLI, with that CLI's own login and its default model, or the one set with claude_model / codex_model / hermes_model in the config (the action's model field is ignored). They run headless from an empty temp dir, with no tools, no MCP servers, no hooks or plugins and no saved session (Codex also gets a read-only sandbox and no web search). The transcript is untrusted input, so the agents get no way to read files or run commands that a spoken prompt injection could abuse. The whole transcript goes in one prompt, without chunking. They can't transcribe audio, so with no -transcriber set they pair with local. No fallback to an API provider.
Hermes runs with --safe-mode, which drops its user config, plugins, MCP servers, AGENTS.md, SOUL.md and memory, and -t none, which enables no toolset (Hermes has no explicit empty toolset, and an empty -t would enable them all). Safe mode also drops the default model, so goscribe passes it: hermes_model and hermes_provider from its config (or -model in assist), else the model.default and model.provider of your Hermes config ($HERMES_HOME/config.yaml, ~/.hermes by default). goscribe then reads Hermes's event stream and fails closed: any event other than plain text and the result (a tool call, say) discards the answer with an error. Hermes has no way to skip saving the session, so it keeps it in its history; --source tool keeps it out of your session lists.
-transcriber only picks who turns audio into text; -provider still runs actions and --auto. local and elevenlabs never fall back to another engine.
local β faster-whisper via the whisper-ctranslate2 CLI, fully offline. Install with uv tool install whisper-ctranslate2 (or pipx). The model is downloaded on first use; uses the GPU when available, otherwise the CPU.
elevenlabs β ElevenLabs Scribe, tuned for meetings: speaker diarization, filler words and false starts removed (no_verbatim), no audio-event tags, and one paragraph per speaker turn with its start time, e.g. [00:12:34] speaker_1: β¦. Optional: -keyterms / elevenlabs_keyterms to favor names and jargon (ElevenLabs adds a 20% surcharge), -num-speakers / elevenlabs_num_speakers to cap the speaker count, elevenlabs_verbatim: true to keep fillers. Needs an ElevenLabs API key. When the ElevenLabs quota is exhausted, goscribe says so and transcribes with local instead (no speaker labels), when faster-whisper is installed; re-run with elevenlabs once the quota is back for the better transcript.
-tags asks the -provider model for tags and writes them as YAML with one list item per line:
date: "2026-10-13"
type: standup
summary: Weekly sync on the design token migration
topics:
- dark-mode
- design-tokens
people:
- Sam
tickets:
- PROJ-123
projects:
- billinggoscribe normalizes what the model returns (kebab-case topics and projects, uppercase ticket IDs, a fixed list of meeting types, sorted and de-duplicated lists) rather than trusting its formatting. To keep tags consistent for grep, it first reads the topics and projects already used in *-tags.yml files under the tags root and its direct subfolders (a records/<date>/ layout) and asks the model to reuse them. Transcripts under 30 words are not tagged. A tagging failure only prints a warning; the transcript and actions are kept.
goscribe search finds meetings by their tags files, newest first:
goscribe search dark-mode # in any field
goscribe search tokens person:alex type:1:1 # all terms must match
goscribe search ticket:PROJ-123 -since 2026-09-01
goscribe search -v onboarding # + topics, people, tickets, related files
goscribe search -paths onboarding | xargs grep -il invoiceWith -v, related files are the action outputs (<name>-*.txt) and notes saved next to the recording as <name>-*.md, such as goscribe assist notes renamed <name>-assist.md.
Terms are case-insensitive substrings, so alex finds both "Alex" and "Alexis". field:value limits a term to topic, person, ticket, project, type or summary. It searches -root, else tags_root from the config, else the current folder, recursively. Exit code: 0 with matches, 1 without (like grep), 2 on errors.
goscribe record records the mic and/or the computer sound (PipeWire/PulseAudio, through ffmpeg) to an mp3 until Ctrl+C:
goscribe record # mic + computer sound -> recording-<date>_<time>.mp3
goscribe record -source system -o webinar.mp3 # computer sound only
goscribe record -duration 45m -transcribe -provider codex -tags
goscribe record -list-sources # pick devices for -mic / -system-quality voice(default) is mono 64 kbps, about 30 MB per hour.-quality musicis stereo VBR.- Press
mto mute or unmute the mic andsfor the computer sound, while recording in a terminal. Muting only affects the recording: that side is recorded as silence, your real mic and speakers are not touched, and the timeline stays continuous. The status line shows(muted), and each change is logged, e.g.π system muted at 00:12:41. - If a side stays silent for 30 s, goscribe warns (
β no sound from mic for 30s) and keeps recording. - The first Ctrl+C stops cleanly; a second one stops at once. An existing file is never overwritten.
- Config defaults:
record_source,record_mic,record_system,record_quality. - Use headphones: with speakers, the mic also picks up the computer sound, so the other people are recorded twice.
goscribe assist is a meeting copilot that only you can see. It listens to your mic (me) and the computer sound (others) with ElevenLabs realtime speech-to-text, one live session per side, so it always knows who said what. Every update, the -provider model refreshes its memory of the meeting and suggests what you could say:
goscribe assist -context ~/notes/standup/ # background notes: a .md file or a folder
goscribe assist -context agenda.md -provider claude -o standup-assist.md
goscribe assist -source system -language fr # only the others, in French
goscribe assist -provider codex -model gpt-6-luna # pick the model (config: codex_model)
goscribe assist -plain # lines instead of the full-screen viewIn a terminal, assist takes the full screen: a top bar (meeting clock, what the model is doing, me/others live or muted), the feed of transcript lines and suggestions with colored badges (ASK, ANSWER, MISSED, RISK, DECISION, RECAP, LIBRARIAN, FIX, FIX?, BRIEF, SOURCE), a dashboard of the meeting (summary, agenda items not covered yet, decisions, open questions, brief, sources, fixes) and a chat input at the bottom.
- Type a question about the meeting and press Enter:
youasks it, and the answer comes from the meeting so far and your context, without waiting for the next update. Plain text is always a question; an empty line does nothing. - Commands:
/helplists the commands and keys,/mute meor/mute othersmutes or unmutes a side (a side that is not captured only says so),/recapasks for a recap of the last minutes (/recap 10mto go further back),/fixcorrects a misheard word (see Corrections below),/briefgives the model standing instructions and facts (see Brief below),/add(alias/context) adds a file, folder or web page to the context (see Adding sources below),/search <question>asks the librarian to look something up in your sources (see Librarian below). - Keys:
Alt+mmutes or unmutes your mic,Alt+sthe computer sound,Alt+wasks for a recap.Tabshows or hides the dashboard when the window is narrower than 120 columns (it sits next to the feed on wider ones).PgUp/PgDnand the mouse wheel scroll the feed; new lines are counted while you read, andEndgoes back to live. - Copying: the mouse scrolls the feed, so hold
Shiftwhile you drag to select text the usual way. - Ctrl+C ends the meeting and writes the final notes (the screen stays until they are written); a second Ctrl+C quits at once and keeps the live notes. Once the screen is gone, the terminal shows the final summary, the action items and the paths of the notes and the transcript. A question already being answered at the first Ctrl+C is still answered, into the feed and the notes; questions still waiting are left unanswered, and a question or
/recaptyped after the first Ctrl+C is refused. When the final notes were not written (second Ctrl+C, a failed final call, too little said), the summary shown is the live one and is labelled so. - With
-plain, or when the output is not a terminal (piped to a file, say),assistprints the feed as lines instead, as below, with a status line in a terminal and the keysm,sandw.
00:12:09 others what about the migration rollback?
β 00:12:15 Ask: Who owns the rollback if the migration fails? β a named owner is required Β· agenda.md
β οΈ 00:12:15 Risk: Monday falls inside the database freeze (20thβ25th). β agenda.md
β
00:13:40 Decision: Ship behind a feature flag
βΌ π 00:58:02 Missed: Hiring plan for Q3 is still on the agenda. β agenda.md
β ASSIST 00:13:52 others βΈ "so the next step would beβ¦" Β· thinking 4s (claude)
- Suggestions are
β Ask,π¬ Answer,π Missedandβ οΈ Risk, at most 2 per update, never repeated;βΌmarks one whose moment will soon pass. An answer always names the context file it comes from: one that doesn't is dropped. Nothing is said or posted in the meeting. - Agenda tracking: the model keeps the agenda items and goals of your context that the meeting has not discussed yet. When the meeting wraps up ("we're out of time", "one last thing"), it raises the important ones as
βΌ π Missed. - Names:
otherscan be several people. When the talk makes clear who speaks (people address each other, or a name fits the attendees in your context), the summary, decisions and action items use the name. - Restoring: naming an earlier assist notes file with
-context(e.g.-context standup-assist.md) puts its fixes and briefs back in force at the start, and the right-hand words of the fixes join the live keyterms. The feed says what came back (βΊ restored 1 brief, 2 fixes from standup-assist.md). The notes file itself is used as reference data, untrusted like a web page (it holds model-written text and answers drawn from documents written by others), while its fixes and briefs are restored as your own; a notes file with two## Fixesor## Briefsections restores nothing, with a warning, and is still reference data. A folder is trusted background as before, except that earlier notes, transcripts and page copies inside it are left out (the startup notice counts them), and notes inside a folder restore nothing. - Keyterms: add a
keyterms: Acme, Globex, Marie Curieline to a context file (in the text or the front matter) and those names are favored by the live speech-to-text, on top of-keytermsandelevenlabs_keyterms. The model also spells sound-alikes as the context does. Live speech-to-text takes keyterms of at most 20 characters: longer ones are left out, with a warning at startup. - The notes file (
assist-<date>_<time>.mdby default) is rewritten after every update: summary so far, agenda items not covered yet, decisions, open questions, every suggestion and your questions with their answers. Its header gives the wall-clock start, to match suggestion times with a recording started at another moment. Ctrl+C writes the final summary and action items into it; a second Ctrl+C quits at once. These notes are a live draft: a summary made from the full recording (goscribewith a summary action) is more reliable. - The transcript is written next to the notes as the meeting goes, one line per committed sentence with its time and speaker:
standup-assist.mdgetsstandup-assist-transcript.md. - Updates run at most every 20 s with
openai/gemini, 30 s withclaude/codex/hermes(-intervalto change). One update at a time; a failed one is retried later with what it missed. - It captures the audio itself, so run it in a second terminal next to
goscribe record, or alone. Same device flags andrecord_*config defaults asrecord(-source,-mic,-system,-list-sources). - When ElevenLabs is out of quota, each side switches to local transcription with faster-whisper (when installed), with a notice: speech is cut at pauses (or every 20 s) and each piece is transcribed with the
basemodel (assist_local_modelto change), less accurate and without partial words. One model stays loaded for the whole meeting (it starts loading as soon as the fallback kicks in), so both sides usually keep up, a few seconds to about 20 s behind depending on the CPU load; without it (no faster-whisper Python found), each piece runswhisper-ctranslate2and reloads the model. If local transcription falls behind, the oldest waiting piece is skipped with a warning. - Needs an ElevenLabs API key (
elevenlabs_api_keyor$ELEVENLABS_API_KEY). Live transcription is billed per stream, so two with-source both. The context is resent with every update: keep it small (a warning shows above 50 KB, 200 KB is the limit). - The transcript is treated as untrusted data: the model is told never to follow instructions spoken in the meeting, and
claude/codex/hermesrun with no tools. - Use headphones: with speakers, the mic also hears the others and their words are labelled
me. - Press
Alt+m(mwith-plain) to mute or unmute your mic (me) andAlt+s(s) for the computer sound (others), or type/mute me//mute others. A muted side is not transcribed: its live session gets silence and stays open, so nothing said on that side reaches the model, the suggestions or the notes until you unmute. Live transcription lags a second or two behind, so the words said just before you press are dropped too. The top bar (the status line with-plain) showsme (muted), and each change is logged, e.g.π me muted at 00:12:41. The keys only affectassist: whengoscribe recordruns in another terminal, mute it there too. - Press
Alt+w(wwith-plain, "what did I miss?") or type/recapfor a short recap of the last minutes, e.g. after a distraction:π 00:42:10 Recap of the last 5 min:and 2 to 6 points, in the language of the meeting. It goes back 5 minutes by default (-recap 10m, orassist_recap: 10min the config);/recap 15mpicks the window for one recap. It is one-providercall: when an update is running, the recap follows it, without waiting for the update interval.
When the speech-to-text mishears a word, /fix <wrong> <right> corrects it, e.g. /fix cash cache.
- It matches whole words, ignoring case, and writes the replacement exactly as you typed it. For a phrase, quote it:
/fix "dark mood" "dark mode". - It applies from now on, and also rewrites what was already said: the transcript file, the notes, the summary, decisions, open questions and agenda, and the lines the next updates see. The model is told about it at the next update. The feed shows a
FIXline with how many past lines changed; lines already on screen keep the old word. Each fix is listed under## Fixesin the notes and underFIXESon the dashboard. - The model may suggest a fix itself when a word clashes with the meeting or your context: it shows as
FIX? dark mood β dark modeand nothing changes until you answer/fix yesor/fix no. Add a number (/fix yes 2) to pick one; without it the latest is used. An accepted suggestion works like a typed fix, and a refused one is not proposed again. /fixalone lists the fixes./fix rm nstops fix n for new lines only: what was already rewritten stays as it is.- Live speech-to-text keyterms are fixed when the connection opens, so a fix does not update them mid-meeting. Add the right spelling to a
keyterms:line in your context for the next meeting.
Typed in the full-screen view's input, /brief <text> gives the model something to keep in mind for the whole meeting: an instruction, e.g. /brief focus on risks to the release, or a fact, e.g. /brief Sam is the PM, Alex is the customer.
- A brief is yours, so the model trusts it, unlike the transcript. It is sent with every update, question and recap, and with the final notes.
- Adding one starts an update at once (once something has been said), or right after the one running, so the next suggestions already follow it.
/briefalone lists the briefs, numbered./brief rm nremoves brief n.- There can be at most 20 briefs of 300 characters each.
- The briefs show under
BRIEFon the dashboard and under## Briefin the notes. - Plain text is still a question: only a line that starts with
/briefis a brief. One typed while the final notes are being written is refused.
Typed in the full-screen view's input, /add <path or url> (alias /context) puts more background in front of the model while the meeting runs, like -context does at the start. Once something has been said, adding a source starts an update at once, like /brief.
- A file or a folder:
/add notes/spec.md,/add ~/notes/acme/. Quote a path with spaces:/add "my notes.md". - A web page:
/add https://example.com/roadmap. A public page is fetched and converted to Markdown, and a copy is saved in the sources folder next to the notes,<notes name>-sources/<slug>.md(e.g.assist-2026-10-29_10-00-sources/). The page is untrusted data: the model reads it as context, never as instructions. Every saved copy starts with a marker line that flags it as an untrusted page copy, so naming one later with-contextor/addkeeps it untrusted and restores nothing. - A doc behind a login (Google Docs, Notion, Linear and the like, any 401 or 403, or a redirect to a sign-in page): with the librarian on, the page is handed to the librarian, which reads the doc with its read-only tools. The feed shows
π asking the librarian for <url>β¦; the doc is your own request, so it does not count towardmax_lookups, runs before any waiting lookup and is never dropped for waiting (but a queued doc is dropped when the meeting ends). If the librarian has no tool that can read it, the feed names the host to add a read-only tool for. Without the librarian, the feed explains how to set it up. /addalone lists the sources added.- Each source shows in the feed with a
SOURCEbadge, and in the notes. - The limit is 200 KB in total, counting
-contextand everything added; a source that would go over is refused. The meeting's own notes, transcript and sources folder cannot be added. - Restoring: naming an earlier assist notes file restores its fixes and briefs, as with
-context, and adds the file as untrusted reference data (listed asearlier notesin the notes); a folder restores nothing and leaves out earlier notes, transcripts and page copies (the feed says how many;/addone directly to use it as reference). A transcript of a meeting named directly is untrusted reference data too (listed asearlier transcript). - Only a line that starts with
/addor/contextadds a source. Schemes other thanhttpandhttps(ftp://,file://) are refused.
The model can look things up in your own sources while the meeting runs (a past decision, a ticket, a document, someone's earlier work). Each lookup is a short, read-only Claude Code session that gets a search question, why it helps now and the running meeting summary, never transcript lines. The answer shows in the feed (π Looking up: β¦, then π Librarian: β¦ β titles), feeds the next updates and is listed under ## Research in the notes.
librarian:
allowed_tools: # the only tools the librarian may use; setting them turns it on
- Bash(gh search:*)
- Bash(gh pr view:*)
- mcp__tracker__search_issues
- mcp__tracker__get_issue
enabled: true # false: keep the tools listed but run without the librarian
instructions: "Acme tickets are PROJ-<n> in the tracker; code and PRs are in GitHub (acme/*)."
mcp_config: ~/.goscribe/librarian-mcp.json # optional, see below
use_claude_settings: false # true: load your Claude Code connectors and plugins, see below
timeout: 3m # per lookup (default)
max_lookups: 8 # per meeting (default)
model: "" # optional Claude model for lookupsgoscribe assist -context agenda.md -provider claude # librarian on: allowed_tools is set
goscribe assist -librarian=false -context agenda.md # this meeting without it-
The librarian is on by default when
librarian.allowed_toolsis set.librarian.enabled: falseor-librarian=falseturns it off, and without tools it stays off (the feed says how to set it up). The header ends withlibrarian: claude, N tool(s)when it is on. -
If a startup check fails (a refused
allowed_toolsentry, a missingclaudeCLI, a badmcp_configor unsafe Claude settings), assist printsβ librarian off: <reason>and runs without it. Passing-librarianexplicitly keeps the strict behavior: the same failure is a usage error and assist does not start. -
Docs the librarian reads (a doc behind a login) are also saved in
<notes>-sources/, like public pages. -
Read-only only. goscribe refuses an
allowed_toolsentry (a warning and no librarian, or with-librarianno start) when it finds one of these:- several rules in one entry. Claude Code splits its tool list on commas and spaces, so an entry must have no
,or space outside its parentheses, and its arguments in a single(...)at the end. - a tool other than
Bash,Read,Grep,Glob,LSand exact MCP tools (mcp__<server>__<tool>).WebFetch,WebSearch,Task,Agentand any other built-in are refused. - bare or wildcard
Bash(Bash,Bash(*)), andBashprefixes that start with a shell, an interpreter, a wrapper or a command that reaches the network or changes files:sh,bash,zsh,dash,fish,ksh,python,python3,node,perl,ruby,php,awk,sed,find,rm,mv,cp,tee,dd,sudo,doas,timeout,nohup,nice,command,exec,eval,env,xargs,curl,wget,ssh,scp,docker,kubectl; andgh api,gh authandgh config. - any entry with a write verb (send, post, create, update, delete, save, write, edit, comment, merge, share, trash, upload, react, schedule).
- MCP tools whose name has no read verb (
get,list,search,read,view,fetch,query,find,retrieve,lookup), and server-wide or wildcard MCP entries such asmcp__trackerormcp__tracker__*.
ghrules must besearch,view,list,status,difforchecks.gitrules must bestatusorls-files:git log,show,diffandgreptake options that write files (--output=<file>) or run commands (grep -O<cmd>), andgit blame --contents <file>prints any file; a prefix rule cannot restrict options or arguments. For the same reason the file toolsRead,Grep,GlobandLSare accepted only with a folder scope, such asRead(~/notes/**)orGrep(//srv/docs/**). In these rules//pathis absolute and~/pathis relative to your home folder: use only these two forms, since/pathis relative to the project root, which for the librarian is a temp folder deleted after each lookup. A bare name, a scope like/**,~/**,**or*, or one containing..would let the librarian read any file (ssh keys, say) into its answer, which reaches the notes and the-providermodel. This check is a heuristic, not a proof: only list tools you know are read-only. - several rules in one entry. Claude Code splits its tool list on commas and spaces, so an entry must have no
-
Two modes. Default, isolated: the librarian loads none of your Claude Code settings, hooks, plugins or permission rules, so Claude Code's own connectors and plugins are not available. It has the shell and file tools you list, plus MCP tools from the servers in the JSON file (Claude Code
--mcp-configformat) thatmcp_configpoints to: only the servers in that file are loaded. Withuse_claude_settings: true, your Claude Code settings are loaded (--setting-sources user), so your connectors and plugins are available, and the servers inmcp_config, if set, are added to them. Your hooks and plugins then run on every lookup, which is slower and may have side effects; aPreToolUsehook, yours or a plugin's, can also approve tools and so widen the tool list, likepermissions.allowrules. goscribe refuses the librarian in this mode if your Claude settings file (~/.claude/settings.json, or$CLAUDE_CONFIG_DIR/settings.json) haspermissions.allowrules, since they would widen the tool list (with-librarianassist then does not start; otherwise it warns and starts without the librarian); it cannot check hooks, so only use this mode with hooks and plugins you trust. Apart from that, only the tools inallowed_toolsare approved. Check which tools answer with a short test meeting. -
mcp_configmay start with~; a relative path is taken from the folder you run goscribe in. If the file is missing, the librarian is refused: with-librarianassist does not start, otherwise it warns and starts without the librarian. -
Tool names can differ in headless mode (
claude -p, which the librarian runs) from what you see in Claude Code: a service available both as a Claude.ai connector and as a plugin shows up asmcp__claude_ai_<Service>__β¦toclaude -p. List the exact names the librarian sees with:# use_claude_settings: true echo "List the exact names of your tools that start with mcp__" | claude -p --tools "" --setting-sources user # isolated mode, with mcp_config echo "List the exact names of your tools that start with mcp__" | claude -p --tools "" --setting-sources "" --mcp-config ~/.goscribe/librarian-mcp.json --strict-mcp-config
-
The librarian's prompt lists its allowed tools, so it runs one plain command at a time instead of pipes or unlisted tools. Calls that the allowlist still refuses show as a dim line in the feed (
π refused 2 tool call(s) not in librarian.allowed_tools: β¦), so you can add what is missing. When the Claude Code session itself fails (an API error, say), the feed shows its reason. -
Data flow: each lookup sends the search question, its "why" and the running meeting summary to the librarian's Claude session, together with your
instructionsand today's date; transcript lines are never sent. The librarian's answers, with their source titles and links, are sent to the-providermodel in the next updates. Withopenaiorgemini, snippets from your sources therefore go to that API. Their text is marked as untrusted data in the prompts, and in the notes links are shown as code, not as clickable links. -
/search <question>asks the librarian on demand. It is your own request: unlimited (it does not count towardmax_lookups), served before the model's lookups and never dropped for waiting (queued searches and docs are dropped when the meeting ends). One librarian runs at a time, so a second search or a login doc waits its turn in the order they reach the librarian (a login doc gets in line once its page has been checked, so a search typed right after it can go first) (the feed showsπ waiting for the librarian: <question>). The answer shows asπ Librarian:, feeds the next updates like any finding and goes to## Researchin the notes; when nothing is found, the feed says so. An exact repeat of an earlier/search(same text, any case) is skipped withπ not looking up "<question>": already asked; a search that failed (β librarian failed) does not count, so you can type it again. The model will not ask a lookup that repeats one of your searches, unless that search failed. A question the context can't answer may start a librarian lookup, which, unlike a/search, counts towardmax_lookups(past the limit it is skipped and the feed showsπ lookup limit reached (N); it is shown asfor your question, never as typed by you). Without the librarian,/searchsays why it is off. -
Cost and latency: each lookup is a Claude Code session of about 30 to 120 s, on your Claude plan. One runs at a time, the newest waiting one is kept (and dropped after 2 minutes), similar questions are skipped, and at most
max_lookupsrun per meeting; a failed lookup still counts (the feed showsβ librarian failed: <cause>), while a waiting one that was replaced or dropped never ran and does not.
goscribe assistreacts with a delay β about 2 s for the transcript, then up to the update interval plus the model's answer time (30β70 s withclaude/codex/hermes), so it is better at "missed" and "risk" than at answering live.-transcriberis CLI only β server mode still transcribes with the request's provider (OpenAI or Gemini).- ffmpeg required for large files β files over 25 MB (OpenAI) or 20 MB (Gemini) are split using
ffmpeg. Install it withbrew install ffmpegorapt install ffmpeg. --autoandactions=autocall the AI API for action selection, which incurs additional API costs.
- Architecture
- Supported formats: mp3, mp4, mpeg, mpga, m4a, wav, webm, ogg, flac, aac, aiff
cmd/goscribe/ CLI entry point and orchestration
cmd/server/ HTTP server + async worker entry point
pkg/config/ Config types, loading, validation (importable)
internal/api/ HTTP handlers and chi router (server mode)
internal/worker/ Async job processor via asynq (server mode)
internal/provider/ Provider routing and fallback logic
internal/openai/ OpenAI API client
internal/gemini/ Gemini API client
internal/elevenlabs/ ElevenLabs Scribe speech-to-text client
internal/whisperlocal/ Local faster-whisper transcription (whisper-ctranslate2)
internal/harness/ Claude Code / Codex / Hermes CLI runner for actions
internal/tags/ Tags files (-tags) and goscribe search
internal/recorder/ ffmpeg recording for goscribe record, live PCM capture for assist
internal/realtime/ ElevenLabs realtime speech-to-text client (WebSocket)
internal/assistant/ Live assistant: memory, prompts, update loop, notes
internal/util/ Shared helpers (model limits, audio splitting, etc.)
make build # build CLI binary
make build-all # cross-compile all platforms
make test # run tests (verbose)
make test-coverage # generate coverage.html
make docker-build # build Docker image
make docker-up # start server + Redis
make docker-logs # tail container logsThanks to all contributors.

