Borrowing it
Nothing to install: this file belongs to michaelwilhelmsen/humla. Take a copy, put it at the same path in your own repository, and replace the rules that are about this project with yours.
curl -O https://raw.githubusercontent.com/michaelwilhelmsen/humla/main/CLAUDE.mdgit clone --depth 1 https://github.com/michaelwilhelmsen/humlaWrote this? Show the measurements
A badge with what this costs and how it scanned, read live from this page, so it follows the numbers instead of freezing them. Markdown for a README, HTML for a documentation site or a project page.
[](https://agentmods.dev/instructions/michaelwilhelmsen/humla/claude-md)<a href="https://agentmods.dev/instructions/michaelwilhelmsen/humla/claude-md"><img src="https://agentmods.dev/badge/instructions/michaelwilhelmsen/humla/claude-md/github.svg" alt="Measured on agentmods" height="20"></a>Or the 80×15 button, for a site that already has a row of RSS and ATOM ones. Only the verdict fits; the numbers stay here.
<a href="https://agentmods.dev/instructions/michaelwilhelmsen/humla/claude-md"><img src="https://agentmods.dev/badge/instructions/michaelwilhelmsen/humla/claude-md.svg" alt="Reviewed on agentmods" width="80" height="20"></a>What it costs to keep this loaded
Counted locally with the o200k_base tokenizer, which is exact for GPT models; Claude uses its own tokenizer and its counts differ. Treat this as one consistent yardstick across the catalogue rather than a bill. Prices are per million input tokens.
| Model | Per session | Once invoked |
|---|---|---|
| Fable 5.1 | $0.31965 | $0.31965 |
| Opus 5.5 | $0.12786 | $0.12786 |
| Sonnet 5.5 | $0.06393 | $0.06393 |
| Haiku 4.5 | $0.03197 | $0.03197 |
Grade A, and why
humla CLAUDE.md scanned grade A with 0 findings against 26 rules in 11 categories — prompt injection, anti-refusal, data exfiltration, privilege escalation, supply chain, agent snooping, system-prompt leakage, SSRF and excessive agency — measured 7d ago.
A static scan of the body, not an audit. Every finding is printed with the line that produced it so you can judge whether it matters here. A mod is markdown that instructs an agent; that is exactly why what it instructs is worth reading.
Nothing flagged
None of the 26 patterns this scan looks for appear in this file: no shell pipes, no recursive deletes, no credential paths, no hidden text, no instruction-override or anti-refusal phrasing, no agent-config snooping. That is not a guarantee, it is the absence of the things that are checkable.
How it starts
The opening of the file, as written. The whole thing — 328 lines — stays where its author put it; the contents beside it link to each section on GitHub.
Humla — project notes
What this app is
Humla is a personal macOS meeting-notes app inspired by Granola. You take freeform notes during a meeting; in parallel the app records mic + system audio, transcribes it, and produces an AI summary that fuses your notes with the transcript. Personal/small-team, not SaaS — your data, your API keys, local SQLite, no backend.
The name is Norwegian for "bumblebee".
Core capabilities
- Hybrid capture (parallel streams) — mic + macOS system audio recorded simultaneously via a Swift sidecar, kept as two separate streams end-to-end (no mixdown). Each gets its own VAD-bounded chunk WAVs, its own full.wav, its own Whisper invocations with its own
prior_contexttrail. In-person meetings produce only mic chunks (system stays silent → no chunks emitted) and the diarizer runs on the mic stream so multiple humans in the same room get distinct labels. Remote calls produce both, and both get diarized — the mic is never assumed to hold a single person just because the system stream has content.Youis earned, not assigned: it survives only when the mic diarize resolves to exactly one voice. - Four STT providers — OpenAI (whisper-1 / gpt-4o-transcribe / mini / diarize), on-device Whisper via Metal, Deepgram (nova-3, nova-2, base), and Groq (whisper-large-v3-turbo). All slot into the
stt::BatchSttAdaptertrait so the dispatch path is provider-agnostic. - Per-language routing —
transcribe_config(typed JSON, single source of truth) is{ default: ProviderConfig, per_language: BTreeMap<String, ProviderConfig> }. Resolution at chunk time: per-note language → per-language override → default. E.g. Norwegian → local NB Whisper, English → Deepgram Nova-3, default → OpenAI whisper-1. - Whisper quality preset — Fast (greedy) / Balanced (beam=3) / Quality (beam=5, low no_speech threshold) for the local provider; bundles sampling strategy + confidence thresholds together so the user picks one knob.
- Per-note transcription language — global Settings → Language is the default; each note has its own language chip that overrides for that note.
- Offline diarization on stop —
speaker-diarizeSwift sidecar runs afterrecording_stop. Two engines (#193, ADR-0005): Nemotron 3 (FluidAudio'sNemotron3Diarizer— NVIDIA's end-to-end diarizer, up to 8 speakers, counts them itself), the default for new installs, and Community-1 (FluidAudio'sOfflineDiarizerManager— community-1 segmentation + VBx clustering with PLDA), which takes the note's speaker count. The engine is chosen per note (diarize::choose_engine, the same answer for every take and the unify pass): a note set above 8 speakers goes to community-1 with the hint, keeping the hybrid split of that total; anything else to the enginediarize_modelselects; and a missing model falls back to the other engine when it is downloaded, rather than writing unlabelled takes — including above 8, where Nemotron's labels are capped at 8 but beat none. A run that fails is retried with the other engine when that one is downloaded (diarize_and_maybe_clean, the one entry every diarize pass goes through): #203's macOS 27 regression failed every Nemotron run on a model marked ready, and withkeep_audiooff nothing could re-diarize those takes later. The pass's engine is a&mutits streams share, so once a retry succeeds the pass's remaining streams run on the fallback; a stream already diarized keeps its labels, which is harmless because each stream's voices are numbered in their own namespace offbuild_hybrid_labels' shared counter. The engine that failed is demoted only when the fallback succeeded on the same audio — both failing is the audio's doing — and only for the launch:choose_engineskips it, which keeps the unify pass, every later take of a replay (each chooses afresh) and later notes on the engine that worked, and a successful download from Settings (diarize_download) clears it. A demoted Nemotron 3 is also re-checked in the background (diarize::recheck, the download's warm-up inference, under the same lock as a download); only when preparing it fails does its warm marker go, every identity's — the sidecar's.humla-warm-failed-record stays, which is what keeps the launch re-warm from retrying it — so a restart doesn't pay the failure again on its first note and Settings offers to prepare it. Both toasts come fromdiarize.rs(fell_back_message,recheck_failed_message), and their engine names fromEngine::label, mirrored byDIARIZE_ENGINE_LABELinsrc/lib/diarizeEngine.ts(pinned by a test reading it). The note's speaker chip (and the import dialog) offers Auto and 1–12 from one list,src/lib/speakerCount.ts, because a count above 8 is what selects community-1.DEFAULT_DIARIZE_MODEL/Engine::from_setting/NEMOTRON_MAX_SPEAKERSare mirrored in TS byDEFAULT_DIARIZE_ENGINE/selectedDiarizeEngine/NEMOTRON_MAX_SPEAKERSinsrc/lib/diarizeEngine.ts(pinned by its test reading the Rust files), andenginesToDownloadthere fetches whatengine_preferencefalls back to — change both. An install that ran before #193 was pinned to community-1 on upgrade (db::migrate_diarize_engine, keyed on whethernotes.sqliteexisted before this launch) and is offered the switch by a card on Home (nemotron_offer), because switching costs a 193 MB download and a one-time Neural Engine compile. Sortformer is removed: a storedsortformerreads as community-1 and its model folder is deleted once (diarize::remove_retired_models). Branches on which streams produced content: mic-only diarizesmic_full.wavand emitsSpeaker 1:/Speaker 2:; when both streams have content, both are diarized and numbered off one shared counter so a mic speaker and a system speaker can never collide on a number (build_hybrid_labels). The old shortcut — hard-label every mic chunkYou:and diarize onlysys_full.wav— meant a single stray system chunk (a notification chime, a few seconds of video) collapsed an entire in-person meeting onto oneYou:line, since presence of any sys chunk routed the recording into remote-call mode.You:is now applied only when the mic diarize resolves to exactly one voice.recording_statusdescribes the live capture and nothing else (#187): it is a single global slot that also drives the tray icon and its Start/Stop items, so only the post-stop chain of the capture that just finished may write to it.ChainProgressis the one type that owns the channel — its bracket and its named steps, since the two are one answer and a pair of types could disagree. One variant per surface:PostStopthe global phase,Rediarizethe per-notediarize_statusevent,Replaythe per-notetranscribe_status(where a deferred run's audio position already rides),Silentnothing at all. It is passed in the wayStopClockalready is, sodiarize_and_apply,rediarize_apply_to_chunksandunify_note_speakers— each shared by all four — never choose the channel;LabelFallbackrides beside it and is orthogonal, describing what a pass does when labels can't be produced rather than where it reports. Re-diarize on the global phase meant re-diarizing note B while note A recorded ended on anIdlecarrying no note id, so A's bar, timer, stop button and tray all read idle with the capture still live. - Speaker echo never becomes a voice (#196) — when a call plays through the speakers the mic records every remote voice again, and a mic diarize counts that echo as people: end-to-end engines found 3–5 voices on a mic with one person at it, and in a hybrid meeting each reads as someone in the room who isn't there. Wherever the mic is diarized beside a system stream —
diarize_and_apply's hybrid branch,rediarize_apply_to_chunks' (the two mirror each other), andunify_applyper take —echo.rsruns first, as the named stepremoving_echo: the take's own lag fromecho-probe's GCC-PHAT track (a window counts only when its neighbours find the same lag, since voiced speech gives chance peaks; none → neither part runs), the echo cancelled out of the mic byecho-probe's MDF canceller and suppressor, and the mic diarize reads that cancelled mic from ahumla-echo-<uuid>.wavin the system temp dir, deleted once the diarize has read it — never underrecordings/ordiagnostics/, where it would sit outsidekeep_audio. Then every mic voice that reads as echo is dropped before the voices are numbered, so its words fall to the nearest remaining voice: a voice is echo when its share of frames in (lag-shifted, energy-detected) sys silence is under half the take's, or the linear canceller removed ≥ 4.5 dB of its energy — the energy only ever adds to the ratio, never overrules it. Guards: under 5 s of sys silence judges nothing, and a take never loses every voice, which is what keeps Community-1's merged voice. The unify pass judges each hybrid take inside its own span of the joined mic, so a voice is dropped only where it was echo — and does so before the system stream's speaker-count hint is taken from the mic's voice count, or every echo voice would cost the remote side one. The thresholds were fitted on two podcast takes from one Mac, so the diarize dump (echo, and<engine>-unify.jsonfor the unify pass) keeps each take's lag and echo removed and each mic voice's seconds, ratio, energy removed and verdict, to be revisited on real meetings;echo_ms/unify_echo_mstime it. An hour of audio takes about 7–8 s and 470 MB on an M1 Max (#200): the lag track's windows run on threads, both streams stay the WAVs' own 16-bit samples, and the suppressor and report run block by block inside the canceller (aec::cancel_threaded), so the cancelled mic is the only whole-take buffer it adds. A take of up to ten minutes is cancelled sample for sample as two whole passes would cancel it, which is what the check's thresholds were fitted on. A longer one converges on its last three minutes of far-end signal, ending on the last block where the far end sounds — converging on through a silent tail decays the filter's power estimate while its coefficients stay, and every range starts from that filter — then is split into ranges of at least three minutes on up to eight threads, each started from it 20 s ahead of its range. Past ten minutes the output is no longer two whole passes' bit for bit, and the diarizers flip on borderline 7–15 s fragments even between equally valid canceller variants, so judge any change there downstream against several baselines, never one.echo-probe'scancelkeeps the two whole passes, for the CLI. The transcript keeps its text dedup: live chunks transcribe before the full streams exist. - Speaker rename + colour-coded pills — each unique speaker gets one of four design-token colours (interactive blue, success green, warning gold, accent red, cycling for 5+). A chip strip above the transcript lets the user click any speaker to rename inline; rename is a regex line-anchored rewrite of the transcript text — no separate metadata table.
- Two-source summaries — model gets
[Notater](typed notes) and[Transkripsjon](transcript) as two labelled blocks in one user message. Preset prompts are deliberately minimal (kind of summary + language only); the only source-trust signal is the parenthetical tags[Notater] (user-written)/[Transkripsjon] (auto)— the old explicit "favour notes for intent, transcript for facts" rule was dropped. An empty side is sent as(ingen)rather than omitted, so thinking models don't second-guess a missing block. - Per-note presets — Meeting / 1:1 / Lecture / Interview / Brainstorm / Voice memo, each with its own summary prompt. Custom prompts also supported (rows in
summary_promptstable, referenced ascustom:<id>). - Custom vocabulary — per-user list of names and tech terms biasing decoding. Threaded through Whisper-shaped providers as
initial_prompt, Deepgram askeyterm(Nova-3) orkeywords(other models) query params. - Trailing transcript context — every chunk's transcription receives the last ~150 committed words as
prior_context(Whisper'sinitial_promptslot for OpenAI/Local/Groq; Deepgram ignores it because itskeywordsis a per-token boost, not a continuation primer). Single biggest mitigation against silence-driven hallucinations and proper-noun drift. - VAD-bounded chunks — sidecar rotates each chunk at natural speech pauses (min 1.0 s / max 15 s / 500 ms silence trigger) instead of a fixed timer.
- Reasoning-model temperature handling — gpt-5 and later / o-series reject
temperature;openai::summarizedetects viais_reasoning_model()and omits. - Local-LLM summary tuning — Ollama path goes through the native
/api/chatendpoint (not the OpenAI-compat shim at/v1) sothinkis a real toggle instead of being template-stripped.num_ctxis sized adaptively from the actual prompt length +num_predictbudget, rounded up to powers of two, bounded[8192, 65536]— a flat 65K KV cache was OOMing the model runner on tighter Macs (the cache is multi-GB on top of model weights).keep_alive: 0releases the model immediately after each summary so RAM frees up. Sampling is model-family-aware (sampling_profile(model, think)): Qwen models get the loop-breaker profile (presence_penalty=1.5,frequency_penalty=0.5,repeat_penalty=1.0) that breaks Qwen 3.5's thinking-phase loop class; every other family (e.g. the recommendedgemma4:12b-mlx) gets neutral defaults (presence/frequency=0,repeat_penalty=1.1) rather than penalties tuned for Qwen. Onlyollama_chat_stream(the summary path) carries this; the agentic tool-calling step (ollama_chat_step) uses neutraltemperature=0. Cloud OpenAI summary streams and sharespost_chatwith the chat path, which retries transientKind::Requesterrors twice (500 ms / 1.5 s backoff) — covering the case where reqwest tries to reuse a pooled HTTP/2 connection that OpenAI's edge has half-closed. Retries can't rescue a connection reaped while the model reasons, which is why the path streams at all; see theopenai.rsrow. - Your notes over MCP (#172) — Humla ships its own Model Context Protocol server, so Claude Code, Codex or any other MCP client can search and read the library from wherever the user works. Six read-only tools (
search_notes,get_note,get_transcript,list_notes,list_folders,list_clients), off until explicitly enabled in Settings → General → Integrations, which also offers a ready-to-paste config snippet per client. Read-only in this version, and no tool returns or references an audio file —keep_audiostays the single absolute gate on audio (#24) and this is not an exception above it. The workspace is resolved, never accepted as an argument, so Personal and workspace notes can't reach each other however a client asks. Search is keyword-only on purpose: an agentic client substitutes repeated queries for vector recall, which buys no API key, no per-query cost, no Keychain prompt and no network. The gap that does bite is cross-language — the index is lexical, so a query in the wrong language returns nothing, and the fix chosen is to carry each note's language as data (lang:on every hit and row,languageas a filter) rather than to name any language in a tool description. Shipping it first-party is a maintenance decision as much as a feature: Granola never did, so its users wrote nine servers against its local store, and the schema became a public API maintained by strangers. - Send a note to your own webhook (#213) — the push half of what MCP does as a pull: ⋯ → Send to webhook… POSTs one note as JSON to a URL the user chose, so a knowledge base, an agent or an automation can pick it up. Manual, every time — no automatic send on stop or on summary: what leaves is a whole meeting, other people's words included, so each send is a confirmed choice, made after the user has fixed speakers and edits. It lives in the ⋯ menu beside Export…, not in the toolbar — it is opt-in, so a bar button would cost every user the toolbar's measured width, and the menu is already hidden on read-only notes. The confirmation (
SendToWebhookModal) names the receiver's host only, never its path or query, which often carry a token. Opt-in twice:webhook_enabled(SQLite, an ordinary setting so the menu can ask without the Keychain) and an endpoint in the Keychain (webhook_config, read throughcrate::keychain), saved by its own button in Settings → General → Integrations, where the form isn't even loaded until the switch is on. Header values are write-only, like API keys:webhook_get_endpointreturns header names, and a header saved with no value keeps the stored one (merge). The backend sends the note from the database, so the view awaitsflushEditsfirst (sendNoteToWebhook,src/lib/webhook.ts); there is no frontend snapshot of the note to drift from it. The payload (build_payload) is a contract the user's receiver is written against: an explicit allowlist underschema_version: 1, documented indocs/webhooks.mdand pinned by a test listing its keys — fields may be added, existing ones keep their meaning. Folder and client go as{ id, name }(an id alone means nothing to a receiver),languagethroughsummary::note_language(the rule the summary and title use), andrecordingcovers takes with index > 0 only (the "Earlier transcript" repair session is not a take), with totalsnullunless every take's time is known. No audio and no path to any;keep_audiostays the single gate on audio (#24). One request, no retries, no redirects, 30 s, and no error ever carries the URL or the receiver's response. - Save a note's audio (#224) — ⋯ → Save audio…, beside Export…, writes the note's stored audio to wherever the user picks, because it lives at
recordings/<note id>/<take id>/playback.wavand nobody can be expected to find that. It saves one file per note: every take'splayback.wav(the mixed file the player plays, and the one every take keeps whether it was captured, imported or synced) joined in take order bywav::concat_pcm16_mono_16k. That copies the samples through without decoding them, refuses a WAV that isn't 16 kHz mono 16-bit (rather than gluing two formats into noise), and builds in a temp dir, copying over the destination only once the file is whole. Index 0, the "Earlier transcript" session, was never a take and isn't counted. M4A is the default — AAC through the/usr/bin/afconvertevery Mac ships, because an hour of that WAV is ~115 MB — and WAV the alternative. It is its own sheet (SaveAudioModal) rather than a checkbox in Export, which writes one document through one save panel. The sheet lists what it would save before it asks where — each take with its length, Missing for one whose audio is gone, and a total (note_audio_export_info, oneTakeAudioper take) — since a file quietly missing two of three takes reads as a bug, and a list shows which one at a glance where a sentence has to be read and counted. Aplayback.wavthe join would refuse lists as missing too, so the list and the saved file can't disagree. A note with none still gets the sheet, with the reason, rather than a greyed menu item, which has no room for one. Disabled while a capture or a replay on the note may still be writing a take. This writes no audio Humla keeps — it copies out whatkeep_audioalready let the device store, at the user's request — so #24's gate is unchanged. - GitHub Copilot through the user's own subscription (#227) — a summary and chat provider (
copilot) that bills the user's Copilot seat instead of an API key. Humla signs in with its own OAuth app (device flow,copilot::auth, client id inCLIENT_ID; the app is registered under the Screenpartner-AS GitHub org) and keeps the GitHub token, refresh token and login in the Keychain (copilot_credentials); tokens last 8 h and refresh without a secret because they came from the device flow, and a refresh GitHub rejects signs the user out. Every call goes through the Copilot CLI driven bygithub-copilot-sdk(runtimefeature only — no bundled 180 MB CLI, and.cargo/config.tomlsetsCOPILOT_SKIP_CLI_DOWNLOADor its build script downloads one). That is not a preference: GitHub refuses thecopilot_internaltoken exchange to apps it hasn't allow-listed, and the GitHub token sent straight to the Copilot API sees only gpt-3.5/4o-mini/4o, whatever the org enabled; through the CLI the same token gets the org's whole catalogue. Each call is its own SDK session with Humla's system prompt inreplacemode andavailable_toolslimited to the tools Humla declares — the CLI's agent prompt and tools cost ~29k input tokens a call otherwise. Chat's tools are declared without handlers, so a step ends at theassistant.messagecarryingtoolRequestsandchat::run_chatdrives the loop as for every provider. One session serves a whole chat turn (ChatTurnSession, held by the per-turnCopilotChatAdapter): the next step hands the tool results back into it (handle_pending_tool_call), which Copilot reports asinitiator: "agent"— continuations rather than new user prompts, the distinction premium-request billing turns on — where a session per step made every step a user prompt.continuationdecides whether a step can continue; the forced wrap-up adds a system turn a session can't take, so that step alone starts a fresh session with the history written out as one transcript (render_prompt). Each model call logs one[copilot] model= initiator= input= output= cost=line, never its content. A CLI that fails to start keeps its error onRuntime;copilot_statusreturns it ascliErrorand the Settings card offers the supported download when the failing CLI is another version. Finding the CLI (copilot::cli):COPILOT_CLI_PATH, Humla's own download, the usual roots, then one login-shell call for its PATH andnpm prefix -g— npm's globalbinis often on no PATH at all. npm'scopilotis a Node loader; Humla runs the platform package's native binary beside it, sonodeis never needed. When none is found, Settings downloads the version the SDK was built against (cli::VERSION) from npm, checks it against a pinned sha512 and GitHub's code signature, and keeps it under Application Support. The CLI is started with--no-auto-update(it would otherwise swap itself for a newer download the SDK's protocol check may refuse),--disable-builtin-mcpsand--disable-client-telemetry. Models arecopilot_model(summary, title) andcopilot_chat_model(chat), defaultauto— not the sharedchat_model, because Copilot's ids overlap Anthropic's and OpenAI's without meaning the same thing. No embeddings, so chat retrieval is keyword-only, as on Anthropic.cargo test --lib copilot_live -- --ignored(withHUMLA_COPILOT_TOKEN) andcopilot_download_liverun the real thing and spend real requests. - Your own OpenAI-compatible endpoint (#173) — a
customprovider for summaries, chat and transcription: one base URL (custom_base_url) used exactly as typed — Humla appends only the route, so every error names the full URL it asked — an optional key in the Keychain (custom_api_key; none means no Authorization header at all), and a model per surface (custom_model,custom_chat_model, and thetranscribe_configentry's ownmodel).localkeeps its stays-on-your-Mac promise; a hosted endpoint never goes through it. Nothing is guessed about the server: chat and summary sendmodel,messages,stream(and chat'stools) and nothing else — notemperature, token limit or reasoning field, no Ollama port detection, no/v1/responsesfallback, whatever the URL or model name looks like (openai::Endpoint::Custom). Transcription asks forverbose_jsonword timings and, on a 400, asks that chunk again in plainjsonand remembers the(URL, model)for the launch (stt/custom.rs) — that model's chunks then carry no per-word highlighting. Chat on it searches by keyword only, as on Anthropic and Copilot. The model listing (custom_list_models) only helps fill the field, since a gateway may serve no/models, and Settings' Test (custom_endpoint::check) reports the listing and a one-line completion separately. Personal-only: workspace chat runs server-side. - Two-factor authentication, and one session that survives a restart (#221; server contract in
humla-cloud/docs/two-factor.md).commands/cloud_auth.rsowns the session: its token is kept in the Keychain (cloud_credentials, beside the email) and renewed withauth-refresh—renewis the rule, tested against a fakeAuthServer. The password is only a fallback, and is dropped the first time the server answers it withtwo_factor_required: each password sign-in of a 2FA account leaves a challenge on the server, and no background task can supply the code. So an account with 2FA on stores no password at all (enable drops it, disable keeps it again), and a session that ends asks the user back (signed_out_email) instead of retrying. The sync worker never signs in:cloud_sync::Configcarries aSessionthe app lends it (AppSessionincloud_worker.rs), a refused token goes back throughrejected, and the worker reportssigned_out/two_factor_requiredrather thanerror. Sign-in is two-step on every surface through one hook and one form (useCloudSignIn,TwoFactorCodeForm):cloud_loginanswers{ step: "two_factor" }and keeps the challenge in Rust,cloud_verify_two_factorfinishes it, andchallenge_expiredsends the form back to the password. The 2FA commands reject with{ reason, message }(CloudError, read in TS bycloudError), worded bytwo_factor_error. A workspace that requires 2FA is hidden from a member without it;cloud_statusreports it aslocked_workspaces(andlocked_current_workspacewhen it is the selected one), the switcher shows it locked, leading to Settings → Account. A change made there while locked is never dropped (requires_two_factorin the worker): a push it refuses is the rule, not the payload, and a deletion that finds nothing to tombstone found a hidden record, not a missing one. A server without the 2FA routes may answer them with its web page, which reads as no requirement. Setup's QR code is drawn in Rust (qr_code,qrcodegen) as an SVG path, so the page injects no markup; recovery codes are saved throughcloud_save_recovery_codes, which refuses anything that isn't one. - A Note names itself (#90) — a Note whose Title Humla owns gets a short phrase derived from its own content — the head of the typed notes, the transcript sampled across its whole length (
sample_transcript: whole up to 3,000 chars, otherwise six non-overlapping windows from the middle of six equal stretches, so whatever took most of the meeting fills most of the sample and the small talk at either end gets at most a window's share), and the summary when one exists. Every trigger reads the Note through the onetitle_input, so a rule about what a title sees can't differ between them.menubar::is_replaceable_titleis the single answer to "may this Title be replaced?": empty, aRecording 19 Aug 14:32timestamp, or theImported audioimport fallback. A real filename stem is the name the user gave the file and is never touched. Two triggers, both content-settled: the post-stop recording chain, spawned concurrently with the diarize pass rather than after it — the transcript is already final by then (recording_stopdrains every in-flight transcribe before it snapshots), and what diarization adds isSpeaker N:prefixes a title has no use for, so waiting cost minutes on a long meeting for nothing — and a debounced body-settled checkpoint in the Note view for a Note that was typed and never recorded. It goes afterrecord_detected_language, which resolvesautoto what was actually spoken, because the title's language directive reads it. Deliberately not on summary completion — summarising is a button the user presses, so that trigger would leave the motivating case (a menu-bar recorder who never opens the Note) on its timestamp forever — and deliberately not during a recording, where it would spend GPU exactly when local Whisper needs it. The call reuses the summary path'sresolve_provider, forcesthink: false, and substitutes a small non-reasoning model when the configured summary model reasons; it does not stream, so the summary panel never sees it. Failure is silent on the automatic path (a user who never asked for a Title must not be told one failed) and loud on ⋯ → Regenerate title, which bypasses eligibility because the user asked. That menu item is disabled while the Note has nothing to read (no notes, transcript or summary — the backend says so in words rather than reporting a model it never asked as having answered nothing), while a capture or a deferred transcription is still changing the transcript, and while a title is already being written. One-shot by construction: once written, a Title is no longer replaceable, so a Note recorded in several takes keeps the first take's Title and regenerate is the escape hatch. The call is bracketed by atitle_statusevent (per-note, likesummary_status), which the open Note view answers by standing a shimmer in for the title box — the text there is about to be replaced, so dimming it would leave the wrong answer on screen while the right one loads.is_replaceable_titleis mirrored in TS asisReplaceableTitle(src/lib/noteSync.ts) — change both. - Transcribe on your terms (#146) — Transcribe manually (Settings → Recording → Audio retention) trades the live transcript for a quiet Mac: the meeting is captured and its per-source streams retained, and nothing reaches a provider until the user presses Transcribe on the note. Gated twice, deliberately.
keep_audiois the outer gate and stays so — deferring on a device that stores no audio would discard the meeting, and force-retaining behind the user's back is the exact class of exception #24 exists to remove (sessions::defer_transcriptionowns the rule; Settings shows the toggle only once retention is on, so the UI and the backend agree). And it applies only to on-device Whisper, resolved through the note's own language: what the setting buys is a Mac that isn't running Metal inference for an hour, and a cloud provider costs the machine nothing, so a per-language override routing one note to Deepgram keeps its live transcript while a Norwegian note defers. The decision is read once, atrecording_start— flipping the setting mid-meeting leaves the running capture alone, since half a transcript is worse than either regime. Each take carries a persistedtranscribedflag insessions.json(defaulting true, so every take in every existing library reads back as transcribed rather than offering to re-transcribe the world), and it is independent of the setting's current value: turning Transcribe manually off later still leaves old takes transcribable. Pressing Transcribe replays each pending take's retained audio through the same decode → VAD chunk → transcribe → diarize pipeline a file import uses, oldest first — because each take's timeline is numbered off the takes before it. Three things make it not-a-recording, each load-bearing: it never takes the single live-capture slot, so replaying an hour of audio doesn't stop the user recording their next meeting; it reports on its own per-notetranscribe_statuschannel rather thanrecording_status, so it can't blank the bar of a recording running on another note (ChainProgress::Replayis what routes the shared diarize path off the global channel, and it stays that way — #187's rule is that only the live capture's own chain writes torecording_status, so the replay's steps are announced on its own per-note channel) — and that channel carries how far the run has got and which half of the take it is in, since a take is replayed and then diarized and the two are minutes apart on a long meeting (#188: 1m48s of diarize sat at 100% under Transcribing…). The measure is audio position (ReplayProgress: each take's duration times the streams it replays, monotonic and clamped in Rust) rather than chunks, whose total isn't known until the run ends; thestepbeside it (#188'stranscribing|diarizing, widened by #189) is what the label and the track follow, becausedone_msreaching the total and the diarize starting are the same reading — every step pasttranscribingkeeps the position it reached (it is where the next take resumes) and goes indeterminate, since the sidecar emits no progress and nothing may invent a percentage. The note's own bar and the app-wide compact pill draw all of it through the oneCaptureIndicator, in the same two places a stop reports — progress has one position whatever kind of work it is, so the Transcript panel keeps its shimmer and draws no second copy. Identifying speakers in take 2 of 3… is the widest label the bar ever carries, which is whyROW_STEPSgained a sixth step (counter): it is the only part of a progress pill that can go, and it fires before the busy pill's word — the one inversion of that ladder's cost order, on the grounds that the take clause is the only one of the two a sighted user can still read afterwards, from the pill'stitle. Every step of a run is also instrumented the way a stop's is (ReplayTimings/ReplayTakeTimings— per take the transcribe, the diarize and theplayback_msinside it, plus the run's ownunify_ms; one stderr line per run plusdiagnostics/<note_id>/replay-timings.json— a sibling file, not a merge into the take's diarize diagnostics, because that one is keyed<engine>-<source>and every take of a run would write it); the ratio between the halves inverts against a stop, which did its Whisper work during the meeting. And its chunks never stream intonote.transcript— the text arrives once, fromrebuild_note_transcriptover every session's timeline, so a replay that dies half-way leaves nothing behind that no timeline accounts for (ADR-0004), and a newly transcribed take lands at its own position in the note even when a later take was already transcribed (whichcombine_with_snapshot, appending to the end, could not). A diarize that can't run is not a failure here (LabelFallback::Unlabelled): the text lands withoutSpeaker N:prefixes rather than being thrown away with the labels. A take that transcribed nothing is deliberately not marked transcribed — silence and "every chunk failed" are indistinguishable from there, and the second has to stay retryable.note_sessionsanswers both halves of "would Transcribe do anything" ascanTranscribe(untranscribed and the raw streams still on disk —session_has_replayable_audio, notsession_has_audio, since a take reduced to its mixedplayback.wavstill has audio and still can't be replayed), so a take swept away by "Delete stored audio" never offers a button that can only fail.- The same machinery re-transcribes, from a
RefreshCwicon button in the Transcript panel beside Copy — mirroring the Summary panel's regenerate, because the two panels' actions shouldn't read as different kinds of control.sessions::TranscribeScopeis the difference and the whole of it (mirrored by the"pending" | "all"union onipc.transcribeNote— change both):Pendingfinishes what a deferred capture left waiting,Allre-runs every take whose raw streams survive (canRetranscribe), which is the fix for a recording that came back off the wrong language or the wrong model.takes_to_transcribeis the single answer both surfaces mirror, and it filters on replayable audio under both scopes so a run can never be offered that would silently skip every take.Allreaches the legacy flat take thatPendingcan't (it reads backtranscribed: true) — deliberately, since a note recorded before per-session storage is exactly the kind whose language needs correcting. And becauseAllreplaces text the note already had whiledb::set_transcriptkeeps no history, the command snapshots a note revision first, so the run is undoable from the note's existing revision list;Pendingneeds none, since it only adds takes that had no text. A take that fails mid-run stops the run without discarding what the takes before it committed — their timelines are written and their text is in the note, so the checkpoints and the asset push still happen, and the failure becomes the outcome only after. - A session's scalar metadata stays immutable,
duration_msincluded.cloud-sync'spush_sessionderives a session's last-write-wins key from its ownstarted_aton the stated grounds that "index / started_at / duration / streams never change", so aduration_mscorrected after the fact re-pushes under a byte-identical key and converges only because the server's hook compares<rather than<=. That is why a deferred take's length is taken from the sidecar'sfull_recordingevent at capture time (CaptureSink::captured_duration_ms→finalize_session, which prefers the timeline and falls back to this) rather than backfilled once its timeline exists.record_transcribed_taketherefore writes the flag and nothing else.
- The same machinery re-transcribes, from a
- Every step of the stop and the replay has a name (#189) — one
Stepvocabulary inrecording.rsthat both chains draw from (transcribing·saving_audio·removing_echo·diarizing·writing_playback·matching_speakers), because the replay's second half is the stop's post-stop chain: the label and the counter come from one place and the client maps one type. A step earns a name only if it can run long enough to be worth one —record_detected_language,finalize_session, the manifest writes and the temp-dir cleanup are all reliably sub-100 ms, and a label that flashes is worse than none. The two that mattered most were both invisible before:unify_note_speakers, a second full diarize over every take's concatenated audio for any note with 2+ takes and plausibly the longest single step either chain runs, and the per-stream diarize count, where a hybrid capture's two sequential diarize passes are a real discrete fraction. A discrete counter is TEXT, never the track's value. Every step emits its counter before that unit's work, so a fraction offindex / countreads 100% with stream 2 of 2 not started and retreats to 50% at the next step's boundary — and(index - 1) / countonly trades that for a bar sitting at zero through the whole first unit, which reads as stuck. SoStepCounterrenders as1/2beside the label (returningNonebelow two units — "1 of 1" is a number about nothing) and the track stays indeterminate for it. The track is determinate for exactly two things, both genuinely continuous: the stop's drain (chunks done over chunks pending) and the replay's audio position.discreteCounterinRecordingBar.tsxis the single place that rule lives — it returns the counter text andvalue: nulltogether, so no call site can pair a counter with a fraction. Payloads stay additive and mirrored field-for-field:step/index/countride theDiarizingphase onrecording_statusand the run's own measure ontranscribe_status, and every bracket serializes byte-identically to before. The pill shows one counter, the coarsest it has — which take of the run beats which stream of the take — and it is the same sliceROW_STEPS.counterhides at the narrowest widths, so a new label cost the row nothing (tightest slack over the sweptrecbar-*scenarios is unchanged at 14px). Instrumentation is the existing one:StopTimingsalready attributedkeep_audio_ms, the per-streamdiarize_mic_ms/diarize_sys_ms,playback_assets_msandunify_ms;ReplayTimingsgained the matchingunify_msand a per-takeplayback_ms, so "which step is slow" stays answerable from data rather than from a second mechanism.stop_steps/replay_stepsare the executable statement of each chain's sequence — test-only, since the chains announce as they go rather than walking a list, so the pair is kept in step with the emitting code by hand. Both they and the live emitter (StreamSteps, over the two optional stream WAVs) derive index and count fromrecording::stream_reports, so a per-stream step counts what is actually present: a stream that isn't there is not counted, a third source is one more flag, and no call site writes1, 2by hand. - A note can be private inside a workspace (#191) —
notes.private, shown and changed from a lock chip in the note meta bar beside the workspace chip, and repeated as a lock on the note's card. Rendered only for a note that has a workspace: a Personal note is private by definition, and a second word for it there would only invite the question of what the difference is. Unlikespeakersanddetected_languagethis is not derived — it syncs, and it bumpsupdated_at, because it records a decision and has to beat a stale copy on another device. The default is shared, and sticky: the last visibility the user chose in that workspace becomes the next note's default (note_private_default:<workspace_id>), per workspace because the honest answer in a client's workspace is not the answer in your own; the menu-bar recorder reads the same rule. Only the author may change it — every member can edit a shared note, so without that guard a colleague could withdraw someone else's note from the whole team by flipping one boolean.- Tightening the server rule stops future reads and nothing else. A record that leaves your view produces no pull event at all, so a teammate who already synced the note keeps a complete local copy, offline, forever.
note_revocations(member-readable, an id and a timestamp, no content) is the only signal that reaches them;note_withdrawnon theSyncObserveris what carries the transition rather than the new value, since only the moment of the flip says to delete anything. Applying one is the single pull path that destroys rather than converges, so it is guarded three ways — same workspace, a non-emptyowner(a note that never synced is ours by construction), and an owner that is not us — and it takes the derived chunks with it, for the reasondb::purge_notedoes (ADR-0002), and every file the note left on disk through the samepurge_note_assetsa Trash purge runs: the crate hands the id back on itspurgedcallback rather than knowing which directories a note keeps. - The chat index is the other half, and it is the one a correct rule set still leaks through. humla-cloud's indexer reads with a service token across every workspace, which no collection rule constrains, so
privateandownerride on every chunk andIndexStore.viewerSqlis composed by every retrieval path — search, the match count, and both speaker listings, a name being as much of a leak as a passage. The viewer is optional in the type and fail-closed in effect: omitted, it matches only public chunks, so a call site that forgets it loses the caller's own private notes rather than showing them someone else's.
- Tightening the server rule stops future reads and nothing else. A record that leaves your view produces no pull event at all, so a teammate who already synced the note keeps a complete local copy, offline, forever.
- The note views are a card grid — All notes and Folder render
NoteCardin a.nd-notegrid, with no date headings: the card carries its own full date (formatNoteDate) because there is nothing above it to say when. Each card is date, title, a four-line excerpt, then state · client · folder. The excerpt prefers the summary's opening line (noteExcerpt) — the distilled version of everything else on the note — falling back to typed body, then to the transcript with speaker labels stripped. "Opening line" is gated, not literal: horizontal rules, model preamble ("Her er …", anything ending in:) and fragments under 20 chars are stepped over — the first of them kept only when the summary holds nothing stronger — and a fragment body likewise yields to the transcript. Both grid views filter on four axes — status, Client, who recorded it, and whether it is filed anywhere. Each axis is its own control because they are independent, and the folder axis is a single No folder toggle rather than a picker: the sidebar already navigates to a folder, so the only reach a folder axis adds is the notes filed nowhere — which is why a Folder view renders the same bar with that toggle off (showUnfiled), every note there being filed already. A folder's filter is cleared when the folder changes: the route's params move under the same component, so without that it silently narrows the next folder and reads as an empty one. The status axis asks what a note HAS, so its rungs overlap — a summarized meeting matches Recorded, Transcribed and Summarized alike (hasStatus). The card's dot cannot work that way and doesn't:noteStateis an exclusive ladder naming the furthest rung a note reached, which is why the two carry separate label maps (NOTE_STATE_LABEL/NOTE_FILTER_LABEL). Reading the filter off the ladder instead was tried and is wrong: under it Recorded means "recorded and nothing since", which on the default settings — where every take transcribes as it records — is a filter that matches nothing at all, and "show me my meetings" becomes inexpressible.recordedis the one rung a note row can't answer, and it comes fromnotes_with_recordings, one sweep of the recordings root askingresolve_sessionswhether a manifest entry exists — a manifest entry, not the audio beside it, so a take swept by "Delete stored audio" is still a meeting that happened. The set lives onuseNotesStore.recordedNoteIdsbecause both grid views draw the dot from it, and it is refreshed on a capture reaching idle and on a replay finishing, either of which can happen from the tray while a grid is the view on screen. The owner axis renders only in a workspace with more than one member, and "Me" claims a note whoseowneris empty — a note recorded here before it ever synced is still yours, and hiding it would drop your newest meetings out of your own filter. Multi-select is unchanged from the old list row (#19): the same Cmd/Shift-click, Space, Esc and checkbox rules, with the checkbox moved to the card's top-right corner.- A borderless card needs a recessed ground.
.nd-welldarkens the scroll region a few percent, because the card's edge is entirely tone against what it sits on — and graphite declares--color-canvasand--color-surfaceboth as#ffffff, which leaves a white card on a white page with nothing but a shadow. Darkening is the one move that works in every theme and both modes; in dark the surface is already the lighter of the pair, so pushing the ground down widens the same gap. For the same reason selection cannot be a fill alone —--color-accent-softis tuned against the canvas, and graphite's#efefeflands darker than the well, so a picked card would recede. The accent ring is what reads everywhere. Everything else the card owns comes from the theme — radius from--radius-card, shadow from--shadow-card— so it is the third inset card in fact and not only in description. Underprefers-contrast: morethe card takes a real border and drops the shadow: tone-as-edge is exactly what that preference asks us to stop doing. .nd-notegrid's track floor isminmax(min(var(--card-min, 300px), 100%), 1fr). Themin()is load-bearing — a bareminmax(300px, 1fr)cannot shrink past its floor and overflows the view sideways once the body column is narrower than one card, which it is at the layout's own minimum width.
- A borderless card needs a recessed ground.
- Folders — flat folder list, per-note assignment; sidebar search matches note titles/bodies/transcripts and renders a flat result list annotated with each note's folder name.
- Chat over your notes — an agentic retrieval loop (
chat/), surfaced as the Chat tab of the Note right panel. The assistant searches and reads notes with three tools (search_notesover FTS5 keyword + semantic embeddings,get_note,list_notes), streams its answer, and cites the notes it drew from as chips that navigate to the source. Two per-conversation retrieval filters live on the conversation row and bind every turn regardless of what the model asks for: breadth (note|folder|all— what is in reach) and the authorship pin (owner_filter, a user id — whose notes are in reach, workspace-only, surfaced as the "Created by me" toggle beside the breadth picker). Multiple conversations per note, with history; a conversation can be renamed or deleted (hard, with a confirm — there is no Trash for chat) from the sidebar row's right-click menu, the/chatapp bar's ⋯, or the note pane's history popover. Runs on a chat provider configured separately from transcription and summary (Settings → Chat; OpenAI or a local server). The local provider is still stored as"ollama"but is not Ollama-only (#179): the adapter picks Ollama's native/api/chaton:11434and plain OpenAI-compat everywhere else, the same callsummarize_with_basemakes — mirrored in TS asisOllamaUrl, which decides only what advice to print. The embedder for semantic retrieval has its own address and name (embed_base_url/embed_model, both blank = the chat server andembeddinggemma), because the chat server is frequently not the one that can embed:mlx_lm.serverserves no/v1/embeddingsroute at all, llama-server wants its own process, and LM Studio names the same model differently. Settings probes it with a real one-word embedding rather than reading the chat server's model listing — a listing cannot tell you a route is missing. A reachable embedder and an embedded corpus are two facts, sincechunk_embeddingsis keyed(text_hash, model)and search filters on the model: naming a different embedder leaves every existing vector unreachable, sochat_unembedded_note_countsays how many notes that is andchat_embed_missingruns the same backfill the next launch would.localChatHintis the one readiness ladder behind both the Settings tab and the Note pane's setup prompt — the two answered the same question in two copies, and only one of them got fixed the first time. In a workspace, turns route to humla-cloud and retrieval happens server-side. Reachable inside a note, at its own library-wide/chatroute, and — scoped to one folder — at/folder/:id/chat("Chat about this folder" on the folder row's context menu, #110).- A conversation's scope is
(tenant, scope, scope_id)—note(the note id),folder(the folder id, #110) orglobal(a fixed sentinel). The three populations never leak into each other, locally or server-side. Breadth is a live filter within a thread, not its identity: a Note's pane can widen to its folder or the library, but a note-less target is pinned to its own reach (allfor/chat,folderfor a folder chat) because with no anchor the target's identity is its reach —pinned_breadth/targetPinsScopeown that rule on each side. Deleting a folder hard-deletes its conversations (they have no Trash and their whole reach was that folder); its notes only reparent, so the Sidebar confirms only when threads would actually be lost.ChatTarget::from_idstakes the note and folder ids as alternatives — both at once is an error, and an empty id is an error under either. - Opening a pane drafts or resumes, by target (
resumes_on_openin Rust,targetResumesOnOpenin TS — mirrored, change both)./chatdrafts: it opens on an unsaved conversation with the prompt cards showing, persisting nothing until the first turn, so an abandoned thread leaves no row and "+" is a local reset that costs no IPC. A note's Chat tab resumes that note's most-recent thread, deliberately — a note is an anchor, so continuing the same line of thinking is the plausible default there. A drafting pane's breadth and authorship pin live in the pane and ride in onchat_send(DraftSettings), which is what preserves the #61/#103 guarantee without the lazy row that used to be hidden from the list yet still resolved to by the next send. Conversation lists hide zero-message rows for drafting targets only (in a note, an empty thread is the draft being resumed) — in SQL for Personal so paging stays honest, and off the server'smessage_countfor a workspace, whose messages never land in the local table.
- A conversation's scope is
- Every word of the transcript has a timeline behind it (#169, ADR-0004) — the styled reader renders from the merged timelines, so text
note.transcriptcarries and no timeline accounts for is invisible there, and the first rebuild (rebuild_note_transcript, reached by cycling a speaker label, deleting a chunk, re-diarizing or unifying) deletes it outright — out of the summary, chat retrieval and embeddings with it. It arose becausecombine_with_snapshotprepends the prior take's transcript whileserialize_timelinesees the current session's chunks alone, so any recording that landed text but wrote no session assets left an orphaned prefix. Three defences, in order:- Prevention.
diarize_and_applyno longer returns early when the diarize model is missing; it serializes the chunks with an empty label — and the label alone, since word timings are orthogonal to who spoke — so the take still writes a session — which also gives a later re-diarize somewhere to attach to. - Repair on open.
note_timeline_repaircomparescomparable_wordsof the transcript against the timelines' projection;orphaned_prefixrecovers the leading lines the projection misses andsynthesize_orphan_timelinewrites them as a session at manifest index 0 (named "Earlier transcript", never "Recording 0" — it was never a take). Idempotent, and it runs after the workspace session pull so a still-arriving timeline can't be duplicated. A note with no timeline at all is left alone — it renders the textarea and hides nothing, and a synthesized session would take its free-text editing away. - Render-time guard. When
coversTranscriptcomes back false — a timeline present but short because malformed lines were skipped, an asset that never downloaded — the note falls back toTranscriptViewover the whole string. The comparison is the backend's; the client never re-derives the grouping rule (Rust groups on label alone,groupTimelineon label and session, deliberately), and comparisons normalize word sequences, never line counts. One more mirrored pair to change together:split_label(Rust) andparseTranscriptLines(Note.tsx) are the same "Label:prefix" rule, and the repair splits lines by the first while the reader draws them by the second. Separately,commit_rebuilt_transcriptrefuses to commit an empty projection over a non-empty transcript, which protects pre-sessions notes that have a transcript and no timeline at all from being blanked by one click.
- Prevention.
- Transcript editing routes through the timeline (#170) — on a note that has a timeline, the styled reader renders from it and
note.transcriptis a projection of it, so editing is per turn: a hover pencil opens that turn in place, and committing callsnote_timeline_set_chunk_textwith the whole run of chunk indices the turn spans (TimelineGroup.indices), which writes the text into the lowest index, empties the rest, and re-derives the transcript from every session's timeline — the same pathnote_timeline_set_chunk_labelandnote_timeline_delete_chunkalready take. Word timings on an edited turn are dropped (they describe words that are gone); its bounds are kept, so it still highlights during playback and only per-word karaoke is lost, on that turn. The old panel-wide textarea wrote the derived copy and touched no timeline, so the edit was invisible in the reader — permanently — while summary, chat and embeddings read the edited string. A note with no timeline keeps the whole-transcript textarea (TranscriptEditor): with no timeline there is no second copy to orphan. Both are locked while a recording is in flight.
What this file has done since we first saw it
Hashed on every crawl. A supply-chain change to an agent config is a question of when, not whether, so the history is kept rather than the latest state alone.
- 7d ago Changed · +2 lines · +449 tokens per session 2913f786c702
- 9d ago Changed · +2 lines · +1,022 tokens per session 8fc22bdb4c5a
- 13d ago Changed · +1 lines · +466 tokens per session f313b81df352
- 19d ago Changed · +1 lines · +624 tokens per session f18c1b8a9aa6
- 21d ago Changed · +1 lines · +112 tokens per session 7995692fcbce
- 23d ago Changed · +2 lines · +917 tokens per session fc31d42fd80a
- 1mo ago Changed · +1,124 tokens per session 720f7e32c6b6
- 1mo ago Changed · +1 lines · +103 tokens per session 75d637d00c1e
- 1mo ago Changed · +3 lines · +2,479 tokens per session 66f39340daef
- 1mo ago First seen · 315 lines · 24,669 tokens per session scan A 5cf917e71917
humla CLAUDE.md is an instructions file published in the GitHub repository michaelwilhelmsen/humla (339 stars, last pushed 5d ago), licensed MIT. It adds 31,965 tokens to every session, about $0.1279 per session on Opus 5.5. A static security scan graded it A with 0 findings. No closer match exists in the catalogue, so it is treated as the original; first seen 2026-08-30.
humla CLAUDE.md compared with instructions from other repositories
| Mod | Type | Stars | Last push | Licence | Scan | Tokens | Works with |
|---|---|---|---|---|---|---|---|
| humla CLAUDE.mdmichaelwilhelmsen/humla | Instructions file | 339 | 5d ago | MIT | A | 31,965 | Claude Code |
| vscode buildNext.instructions.mdmicrosoft/vscode | Instructions file | 195k | yesterday | MIT | A | 9,719 | GitHub Copilot |
| next.js AGENTS.mdvercel/next.js | Instructions file | 143k | today | MIT | A | 7,461 | Codex, OpenCode |
| langchain AGENTS.mdlangchain-ai/langchain | Instructions file | 149k | yesterday | MIT | A | 4,680 | Codex, OpenCode |
| vscode oss-third-party-notices.instructions.mdmicrosoft/vscode | Instructions file | 195k | yesterday | MIT | A | 5,516 | GitHub Copilot |
| vscode copilot-instructions.mdmicrosoft/vscode | Instructions file | 195k | yesterday | MIT | A | 3,093 | GitHub Copilot |
| deepseek-harness AGENTS.mddeepseek-ai/deepseek-harness | Instructions file | 274k | 2d ago | MIT | A | 4,313 | Codex, OpenCode |