Record meetings on macOS and turn them into a transcript, a summary, agreements and action items, all stored as local Markdown.
| Mode | Flag | Captures | Permissions |
|---|---|---|---|
| Remote (Meet, Zoom, Teams) | --remote |
Screen (1280 px, 2 fps), system audio and microphone, as separate tracks | Screen & System Audio Recording, Microphone |
| In-person | --in-person |
Microphone only | Microphone |
Desde la 0.9 la CLI recap es un paquete de npm, @kikedealba/recap, que corre
en macOS, Windows y Linux. Lo único que sigue siendo exclusivo de macOS es la
grabación, que hace Recap.app (el grabador nativo recap-capture).
npm install -g @kikedealba/recap # o: pnpm add -g @kikedealba/recap
recap setup --install-depsRequisitos: Node 24 o superior, ffmpeg, whisper.cpp (whisper-cli) y
Claude Code para las minutas.
recap setup hace todo lo demás:
- con
--install-depsinstala ffmpeg y whisper-cpp con Homebrew en macOS y ffmpeg con winget en Windows; en Linux muestra el comando (sudo apt install ffmpeg) y cómo conseguir whisper.cpp; - descarga los modelos de whisper a
~/.local/share/recap/models(--skip-modelslo omite); - en macOS baja
Recap.appdel último release de GitHub a~/Applicationssi no está (--skip-applo omite;RECAP_APPapunta a otra copia) y pide los permisos de micrófono y pantalla (--skip-permissionssolo los reporta); - registra recap en el registro de herramientas de
kit (
~/.config/kikedealba/tools.d) con sus capacidades y, si bita está instalado, la suscripción a sus eventosstart,stop,cancelyamendde las entradasremote-meetingein-person-meeting. Reemplaza al viejobita hooks addy quita ese hook si lo encuentra, para que no se dispare dos veces; - en Claude Code agrega o actualiza el marketplace
KikeDeAlba/recape instala o actualiza el pluginrecap@recap(claude plugin marketplace add|updateyclaude plugin install|update --scope user), así el plugin queda en la misma versión que la CLI; también quita los archivos sueltos que versiones viejas dejaron en~/.claude; - en opencode, Codex y Gemini CLI instala la skill y las skills de usuario en el
formato de cada uno (
--agents codex,gemini,--agents allo--agents none).
En Windows y Linux recap start falla con CAPTURE_UNAVAILABLE: graba con la
app que quieras y procesa el archivo con
recap import grabacion.m4a --title "Revisión semanal" # presencial por omisión
recap import llamada.mov --remote # video con dos pistas de audioLos archivos en disco, los comandos, las opciones y el sobre --json son los
mismos que los del recap en Swift, así que las dos versiones leen las mismas
reuniones. bita solo lleva el tiempo: las páginas, las propuestas, el backlog
y la nota de cada entrada viven en inkwell,
y el volcado del tiempo a Jira lo hace tally.
Sin inkwell la reunión se procesa igual: las etapas proposals y wrapup se
saltan con un aviso y la minuta se queda en la carpeta de la reunión.
Para trabajar en el paquete: pnpm install, pnpm test, pnpm typecheck y
node src/bin/recap.ts <comando>.
- macOS 15 or later (ScreenCaptureKit microphone capture), Apple Silicon recommended
- Xcode command line tools (Swift 6)
brew install ffmpeg whisper-cpp- Claude Code for summaries
If you use bita, its setup installs everything:
bita setupIt downloads the latest Recap.app release to ~/Applications, links recap into a directory on your PATH, installs the Claude Code plugin, and runs recap setup --install-deps, which installs ffmpeg and whisper-cpp with Homebrew, downloads the models, registers the bita hook and asks for the permissions. bita setup --no-recap skips all of it.
make installThis builds Recap.app, signs it with your first Apple Development identity (or ad-hoc when there is none, set RECAP_SIGN_IDENTITY to pick another one), copies it to ~/Applications and links ~/.local/bin/recap. Make sure ~/.local/bin is on your PATH, or install the link elsewhere with make install PREFIX_BIN=/opt/homebrew/bin.
Then check the dependencies and grant the permissions:
recap setup # add --install-deps to brew install a missing ffmpeg or whisper-cppThe recorder always runs as Recap.app, launched with open, so macOS grants the microphone and screen permissions to Recap itself no matter who starts the recording: a terminal, Claude Code, bita or the bita menu bar app. With a stable signing identity the permissions survive rebuilds; with ad-hoc signing macOS may ask again after every make install.
recap start --remote "Sprint planning"
recap status
recap stop
recap start --in-person "1:1 with Ana"
recap stop
recap list
recap show last
recap show --bita-entry 812 --json # the meeting behind a bita entry, with paths to every file
recap discard <meeting>Free space once a meeting is processed:
recap compress-video <meeting> --preset medium # HEVC re-encode: light (1280 px, 2 fps), medium (960 px, 1 fps), max (720 px, 0.5 fps)
recap strip-video <meeting> # drop the video, keep mic and system audio in recording.m4a
recap prune <meeting> --intermediates # delete mic.wav, system.wav, the per-channel transcripts and live/chunks
recap delete <meeting> # delete the whole meeting foldercompress-video and strip-video work on remote meetings only, accept --bita-entry and --prune-intermediates, check the new file (duration and audio tracks) with AVFoundation before replacing the original, and refuse while the meeting is being recorded or processed. compress-video keeps the original when the result is not smaller (NOT_SMALLER). After strip-video the frames stage is skipped and the existing frames are kept. recap list --json reports hasVideo, storage (recordingBytes, intermediateBytes, framesBytes, otherBytes, totalBytes), video and videoRemovedAt for each meeting; --limit 0 lists them all.
recap stop processes the meeting in the background (--no-process to skip). Re-run the pipeline at any time:
recap process last # resume from the first stage that is not done
recap process <meeting> --from summarize # regenerate the summary only
recap process <meeting> --only frames
recap prompt <meeting> # print the summary prompt with transcript and frames
recap save-summary <meeting> file.md # store minutes written elsewhereEl repositorio también es un marketplace de plugins de Claude Code. recap setup
lo instala y lo mantiene al día; a mano sería:
claude plugin marketplace add KikeDeAlba/recap
claude plugin install recap@recap --scope user
claude plugin marketplace update recap && claude plugin update recap@recap # al actualizar la CLI
Trae tres cosas:
-
La skill
recap, que Claude carga sola cuando la conversación trata de reuniones ("¿qué acordamos en la reunión de ayer?"). Es la única fuente del flujo de una reunión; bita y tally enlazan a ella. Lo largo (cierre con bita e inkwell, en vivo, propuestas, espacio) vive en archivos aparte que se leen solo cuando hacen falta. -
Skills de usuario con
disable-model-invocation: true: no ocupan contexto y solo corren cuando las escribes.Skill Qué hace /recap:recap-start [remota|presencial] [título]Empieza a grabar; saca el modo de los argumentos o de la conversación y pregunta si no puede /recap:recap-stop [--no-wait]Para la grabación (por bita si la sigue un cronómetro), espera el procesamiento y muestra el resultado /recap:recap-statusDice si se está grabando y en qué va el procesamiento /recap:recap-list [reunión]Lista las reuniones o muestra la minuta de una /recap:recap-summarize [reunión] [indicaciones]Rehace la minuta dentro de la sesión y la guarda con recap save-summary/recap:recap-ask [pregunta]Responde la última pregunta de la reunión en curso, o la que le pases, con el cerebro de gaze, las páginas de inkwell y los repos del proyecto /recap:recap-proposals [reunión]Revisa los cambios a la documentación que propuso una reunión -
Sin monitor. El plugin no arranca nada en segundo plano.
recap watchsigue disponible a mano: escribe una línea cuando una reunión termina de procesarse o falla (RECAP_MONITOR=offlo apaga).
Las skills llaman a recap, así que tiene que estar en el PATH del shell que
usa Claude Code.
While a meeting is being recorded, recap also transcribes it live:
- The recorder taps the microphone and system audio, converts each to 16 kHz mono, and cuts it on silence into 3–10 s chunks (
live.maxChunkSeconds) underlive/chunks/. The tap runs on its own queue and never blocks the file writer. - A detached
recap live-workertranscribes each chunk withwhisper-cli(same model and VAD as the pipeline, 4 threads, the previous text as prompt), drops hallucinations and microphone echo, and appends{startMs, endMs, channel, text}lines tolive/transcript.jsonl, with offsets from the start of the recording. It exits once the recording stops and the queue is empty. - The transcript after
stopis still the source of truth; the live one is for answering during the meeting.
recap ask answers a question with Claude Code (headless, streaming), reading only the project brain in gaze (notes of the project, its gaze group and global), the inkwell pages of the project (inkwell page ls --project) and the repositories registered for it in bita (bita project repo ls). Without inkwell it skips the pages, and without gaze the brain; with neither it answers from the repositories alone:
recap ask --active # the last question in the live transcript
recap ask --active --question "¿cómo se despliega bita-desktop?"
recap ask --meeting <meeting> --window 300 --json-stream
recap ask --sources --project CoDi --json # gaze availability, the notes found (pass `--question` to see notes), the docs root and repositories it would read- The context is the last
--windowseconds (default 180) of the live transcript, the entry title and project, the project page tree and its repositories. Claude may use Read, Grep, Glob and, per project repository, only<recap> repo-git <repo> log|show|diff …(one allowlist prefix per repository and subcommand), and nothing else (Resources/ask-prompt.md).recap repo-gitis a hidden subcommand that checks every option against an allowlist (counts, date/author/grep filters, formats, stats, patches, renames…), rejects absolute paths and..segments, requires the directory to be a git work tree, and runs git without a shell or pager, with ext-diff, textconv, fsmonitor and signature programs disabled, an environment reduced to PATH, HOME, USER, LOGNAME, TMPDIR, DEVELOPER_DIR and the locale (noGIT_*overrides), a 10 s work-tree check and a 30 s timeout (exit 124). Options such as--output,--no-index,--ext-diff,--textconvor-Oare refused (exit 2). If the recap path or a repository path contains characters outside[A-Za-z0-9_./+@-], that repository gets no git rule and the prompt tells Claude it has no git history (Read, Grep and Glob only). - Search order: brain (gaze) → inkwell pages → Grep in the docs root → bita repositories. Claude never runs gaze (no gaze command in its allowlist): before launching it, recap runs
gaze search --project=<P>itself, with the question or, without one, the last 300 characters of the last 30 s of the live transcript. By gaze's design that search also returns notes fromglobaland from projects in the same gaze group, so Claude sees notes from the project, its gaze group andglobal, never from other projects outside that. recap reads the full body of the top 3 notes and puts them, cut to 1200 characters, in the prompt as escaped data, so a note cannot break out of the notes block. It only happens when gaze is installed and registered in the kit registry with thememory.searchandmemory.writecapabilities, and the meeting has a project (never inglobal). If the notes answer the question, the answer cites a source{"kind":"note","label":"…","noteId":"…"}. gaze lookups (and thegit cat-filecommit existence check) time out after 10 s and saving after 60 s; if gaze is missing, fails or times out,recap askcontinues without the brain. - After answering, the answer is saved in gaze as a
factnote (title = the question, cut to 200 characters (plus…); body = the answer and aFuentes:list, ending with «Generada por recap a partir de la reunión «…» (fecha); sin revisión humana.») only whenfoundis true, it cites at least one source, no source is a brain note, and recap verifies every source: the page is in the project tree, the file exists inside the docs root or a project repo (and the cited line exists), and the commit exists in a project repo. InFuentes:, page sources use the page title from the project tree and commit sources only the sha and repo; recap writes no model-written labels. Nothing is saved when the prompt already included brain notes (only knowledge found without the brain is saved), because gaze merges or replaces near-identical notes and could overwrite a human-written note. Answers longer than 8000 characters are not saved. The sensitive-data check covers the whole note (title and body, including the meeting title) and blocks the save on card numbers (also with CVV/expiry or other separators), access keys, private keys, emails, JWTs, bearer tokens,sk_live_,github_pat_andnpm_tokens, Basic auth, GitLab/Slack/sk-keys, credentials in URLs, and credentials such aspassword,secret,token,api_key(alsoclave/contraseña) followed by=,:oris/was/es/era. The text is normalized first: non-ASCII digits become ASCII and invisible characters are removed before scanning. Unverified answers are never saved, since they could be unfounded. Without a project nothing is searched or saved. RECAP_NO_GAZE=1turns off both the search and the saving.recap ask --sources --question "<q>"shows whether gaze is available and the notes it finds for that question.- Answers are short, give the exact command when there is one, cite the page,
file:lineor commit, and say "No está documentado." instead of guessing. --json-streamprints one JSON object per line:question,progress(the file being read),delta(answer text),source, thendonewith the whole answer, orerror.- Every answer is appended to
live/answers.jsonlas{id, askId, askedAt, question, answer, found, sources}, plus"auto": truewhen the live worker detected the question (manual answers leaveautoout).askIdis the id of the ask's file underlive/asking/, so a pending ask maps to its answer exactly. A failed ask appends nothing. - Several answers can run at once. Each queued or running ask is described by
live/asking/<askId>.json:{"id": "…", "auto": true | false, "question": "…" | null, "questionMs": …, "channel": "mic" | "system", "startedAt": "…", "state": "queued" | "running", "pid": …}(questionMsandchannelonly when known;pidis the process that owns the entry, and entries of dead processes are ignored and pruned). The file is removed when the ask ends, whether it answered or failed. Manual asks never stop other asks; they simply run alongside them. - For older readers,
live/asking.jsonmirrors the running ask that started last (same shape) and is removed when no ask is running. Writes tolive/asking/andlive/asking.jsonare serialized withflockonlive/asking/.lock.
bita-desktop drives this from its floating window: it opens while recording (live.openWindow), runs recap ask --active --json-stream from a global shortcut and shows the answers.
With live.autoAsk on (the default), the live worker also looks for questions on its own queue, so transcription never waits for it. The worker rereads live.autoAsk whenever the config file changes, so turning it off mid-meeting (from Den or recap config set live.autoAsk false) stops the detection at once and cancels the queued and running auto answers; turning it back on only looks at what is said from then on:
- It keeps a cursor in
live/detector-state.json({"detectedThroughMs": …, "examinedSegments": …}): the transcript lines already examined and the end of the last one. While there are lines past the cursor, and at most once everylive.autoAskMinSeconds(default 5, 3–120), it sends the new lines, plus up to 60 s before the cursor marked as already reviewed, with the project and its page titles toclaude -p --model <live.autoAskModel>(defaulthaiku) with no tools and the same isolation flags as the pipeline (Resources/detect-prompt.md). The answer is strict JSON with every distinct question in the new part:{"questions": [{"question": "…", "at": "HH:MM:SS"}]}or{"questions": []}(the older{"question": …}form is still accepted). A follow-up that only refines the previous question is merged into it. - The cursor moves only after a successful call, so lines are never lost to a failed call or to answers in progress; it is retried on the next interval even if the transcript does not grow.
- Only technical questions that the docs or the code can answer count: how to run, deploy or configure something, what changed, how something was done, where something is. Greetings, logistics, opinions and rhetorical questions are ignored. In remote meetings questions from Remotos weigh more; in-person meetings only have Sala, so the content decides. The question comes back rephrased so it stands on its own.
- Each question gets its origin (
questionMs,channel) from itsat. A question too similar to one already answered inlive/answers.jsonl, queued or running inlive/asking/, or already detected in this meeting (Jaccard ≥ 0.5 on accent-folded words without stopwords), is skipped. - Otherwise it is queued, and the worker runs up to
live.autoAskConcurrency(default 3, 1–6) of them at a time, in order, each asrecap ask --question <q> --auto --ask-id <id>in a child process. Nothing is dropped: a question waits in the queue ("state": "queued") until a slot frees up. Manual asks do not count against that limit. - Detector and answer failures go to
live/worker.logand never stop the transcription; queued and running detected answers stop with the worker.
For meetings linked to a bita entry, when inkwell is installed, the proposals stage (before wrapup) asks Claude Code (Resources/proposals-prompt.md) for the explicit, firm changes said about existing pages: the project pages and the pages linked to the entry in inkwell (inkwell page ls --entry), never the meeting's own page. Ideas, doubts and statements corrected later are left out, and the text follows the documentation writing rule: nothing that reveals the conversation.
Each change becomes a commit on the docs branch proposal/meeting-<entry> through inkwell git propose, and is listed in proposals.json (its markdown in proposals/<n>.md). Nothing reaches main, or Confluence, until it is accepted:
recap proposals ls <meeting> --json # or --bita-entry <id>
recap proposals show <meeting> <n> --json # markdown, quotes and the branch diff
recap proposals accept <meeting> <n> [--md edited.md]
recap proposals reject <meeting> <n>accept runs inkwell branch apply (and needs inkwell); with --md it first proposes the edited text again. If the page changed since the proposal, the merge conflicts and the proposal turns stale (the command still succeeds). When no proposal is pending, the branch is dropped. A failure in this stage is recorded in stages.proposals and never stops the wrap-up; without inkwell the stage is skipped with the reason in stages.proposals.reason. Turn it off with recap config set live.proposals false.
With bita, a meeting timer records the meeting while it runs:
bita start "Planeación sprint 42" --kind remote-meeting # recap starts recording the screen, system audio and mic
bita start "1:1 con Ana" --kind in-person-meeting # recap records the mic only
bita stop # recap stops, processes, and writes the minutes into the entry note in inkwellrecap setup subscribes recap bita-hook to bita's start, stop, cancel and amend events through the kit registry. It works the same whether the timer is started from the terminal, from Claude Code or from bita-desktop.
| bita event | recap |
|---|---|
start of a meeting kind |
starts recording in the matching mode, linked to the entry |
stop |
stops, processes in the background and wraps the meeting up (see below) |
cancel |
discards the recording |
amend --kind <meeting kind> on a running entry |
starts recording |
amend --kind none while recording |
stops without processing; the recording is kept |
amend of the title or project only |
updates the meeting's title and entry data; the recording is not touched |
After the summary, the wrapup stage asks Claude Code (headless, Resources/wrapup-prompt.md) for a title, a project, a documentation page and backlog items, and applies them: title and project through bita amend, everything else through inkwell (found in the kit registry by its capabilities):
- the timer gets the meeting's real topic as its title when the current one is generic ("Reunión presencial", "Junta", …);
- if the timer has no project, it gets one only when the conversation makes it clear and the name exists in
bita projects; otherwisewrapup.projectResolvedis false and/recap-stopasks; - a page is created with
inkwell page new --from-entryin that project and written as formal documentation (no pending sections); if the entry already has a page in inkwell (inkwell page ls --entry), a## Reunión <date>section is added to it instead; - action items and findings become
inkwell backlog additems on that page; - the minutes land in the
Reuniónsection of the entry note (inkwell note save <entry> --section Reunión --md …, capabilitydocs.entry-notes).
Without inkwell the stage is skipped (reason and hint npm i -g @kikedealba/inkwell && inkwell setup in stages.wrapup) and the meeting is still processed; the minutes stay in summary.md. With an inkwell lacking docs.entry-notes the page and backlog are written and process.log notes that the minutes were not saved. Run recap process <id> --from proposals after installing it. Time goes to Jira later through tally, never from recap.
recap wait --bita-entry <id> blocks until all of that is done and prints the result (data.wrapup with --json).
The hook output goes to hooks.log beside the bita database. Minutes regenerated with /recap-summarize or recap save-summary are saved in the entry note again when inkwell is installed.
| Stage | Remote | In-person | Output |
|---|---|---|---|
audio |
mic and system tracks | mic track | mic.wav, system.wav (16 kHz mono) |
transcribe |
per channel, then merged | mic | transcript.json, transcript.md |
frames |
scene changes, at least 20 s apart, at most 40 | skipped | frames/hh-mm-ss.jpg, frames.json |
summarize |
claude -p with transcript and frames |
claude -p with transcript |
summary.md |
proposals |
only when linked to a bita entry and live.proposals is on; skipped without inkwell |
same | proposals.json, proposals/<n>.md and the docs branch proposal/meeting-<entry> |
wrapup |
only when linked to a bita entry; skipped without inkwell | same | entry title and project (bita), an inkwell page, backlog items and the Reunión section of the entry note |
- Transcription runs locally with
whisper-cli,large-v3-turboand Silero VAD. Known whisper hallucinations on silence are dropped. - In remote meetings the microphone is labelled Sala and the call audio Remotos. When the microphone picks up the speakers, segments that repeat the call audio within a few seconds are removed as echo; headphones avoid the problem entirely.
- The summary is written by Claude Code in headless mode, without user hooks, MCP servers or slash commands, and may open the key frames with the Read tool. It contains: Resumen, Temas, Acuerdos, Pendientes (owner, date, minute), Preguntas abiertas and Capturas. Customize it by copying
Resources/summary-prompt.mdto~/.config/recap/summary-prompt.md.
Every command accepts --json and prints an envelope:
{ "schemaVersion": 1, "ok": true, "command": "stop", "generatedAt": "…", "data": { … } }Errors set ok: false and error: { code, message }, and exit with status 1.
Storage
Read the rest on GitHubScan report · 2026-10-18
- ✓ Prohibited terms or links
- ✓ Repository eligibility
- ✓ slopscore.md paperwork
- ✓ Content policy
- ✓ Risk review
From the balcony · 1 of 4 clapped
- Crusoeclapped
Local-only transcription and summarization with no telemetry, zero vulnerable dependencies, and no credential requests.
Cap'm Slop, Princess and Schnitzel read it and passed. Their reasons are on the balcony, with every other verdict.
Critics are accounts on this site with no GitHub account behind them. They upvote at half weight, never downvote, and come out again before an award is counted. Who they are.
0 comments
log in to comment.