Reference

Every command, flag, source, and environment variable. For a guided walkthrough, start with GETTING_STARTED.md instead.

Global flags

Every command accepts these.

Flag Default What it does
--data-dir ~/.whodar Directory holding the index and feedback files.   
--policy strict Egress policy: strict, redacted, or open.
--pretty off Indent JSON output.
--human off Force human-readable output, even when piped.
--json off Force JSON output, even at a terminal.

Without --human or --json, output is human-readable at a terminal and JSON when piped or redirected, so a person reads a clean answer and a script gets the same machine format as before.

Sources

Each source is one connector against a single interface; all of them merge into one index with --merge. People join across sources by email, or by an alias file when a source only knows a handle.

Source Reads Credentials Dated
org-csv Org chart CSV: names, titles, teams, topics none no
matters Time-entry or matter CSV from a billing or practice system none no
codeowners CODEOWNERS paths per owner none no
git Commit authors per changed path none yes
slack Users, channels, message history WHODAR_SLACK_TOKEN yes
github Repos, contributors, PRs, issues, CODEOWNERS WHODAR_GITHUB_TOKEN yes
jira Issue assignees and reporters WHODAR_JIRA_* yes
confluence Page creators and editors WHODAR_CONFLUENCE_* yes
pagerduty Services and current on-calls WHODAR_PAGERDUTY_TOKEN no
json JSON array of records from a file or stdin none no
graph Microsoft Graph users and reporting lines WHODAR_GRAPH_TOKEN no

Dated sources decay: see recency. Undated sources describe the present and keep full weight. Bot accounts (dependabot and friends) are skipped in the git and github sources.

With --episodes, four of them also record the past work behind the graph, which is what whodar recall points back at: Slack threads and runs of channel conversation, merged GitHub pull requests, resolved Jira issues, and resolved PagerDuty incidents.

whodar index

Builds or extends the index from one source per run.

whodar index --source SOURCE [scope flags] [--merge]
Flag Default Applies to What it does
--source org-csv all Which source to ingest.
--merge off all Add to the existing index instead of replacing. Re-indexing an incremental source this way fetches only what changed.
--full off git, jira, confluence, github, slack With --merge, re-read everything and recompact instead of only what changed.
--allow-shrink off all Accept a source returning far less than last time, otherwise refused as a truncated read.
--aliases all JSON alias file joining one person across sources.
--half-life-days 180 all Days for a dated record's weight to halve; 0 disables decay.
--changes-file all Write the joiner and leaver diff as JSON.
--embed off all Generate embeddings via Ollama for semantic search.
--embed-model all Ollama embed model (default nomic-embed-text).
--ollama-url localhost all Ollama base URL for --embed.
--file org-csv, matters, codeowners, slack-export Path to the CSV, time-entry export, CODEOWNERS file (or repo root), or Slack export zip (or its unzipped folder).
--episodes off slack, slack-export, github, jira, pagerduty Record past conversations so recall can point back at them.
--archive off slack Keep the words of each conversation, not just a link. Needs a Memory license and an encryption key; implies --episodes.
--max-episodes-per-channel 200 slack Conversation cap per channel.
--max-archive-messages 50 slack Retained message cap per conversation.
--include-private off slack, slack-export Ingest private channels if policy allows.
--slack-join off slack Self-join public channels the bot is not in (needs channels:join; posts a join notice per channel).
--since-days 180 slack History window in days.
--max-messages 5000 slack Message cap per channel.
--repo github Repo as owner/name, repeatable.
--github-org github Index every repository of an org.
--max-repos 0 = all github Cap repositories taken from --github-org.
--github-emails off github Resolve user emails to join other sources.
--jira-project jira Project key, repeatable.
--jira-jql jira JQL query; overrides --jira-project.
--jira-url jira Site URL; or WHODAR_JIRA_URL.
--max-issues 1000 jira Cap issues read.
--jira-server false jira Self-hosted Server/DC (v2 API, bearer/anonymous).
--confluence-space confluence Space key, repeatable.
--confluence-url confluence Site URL; or WHODAR_CONFLUENCE_URL (falls back to the Jira URL).
--confluence-cql confluence CQL query; overrides --confluence-space.
--max-pages 2000 confluence Cap pages read.
--confluence-server false confluence Self-hosted Server/DC (REST at root, bearer/anonymous).
--repo-path git Local repository root, repeatable.
--git-since-days 365 git History window in days.
--git-until-days 0 git Stop the window short of today, for a past view.
--max-commits 2000 git Commit cap per repository.
--git-workers machine git Commits diffed at once; a walk's whole cost.

Incremental re-indexing

Re-indexing git, Jira, Confluence, GitHub, or Slack with --merge is incremental: it fetches only the items changed since the last run and folds them into the index, keeping everyone it did not re-read. A per-source watermark is kept in index.state.json beside the index. Other sources always read in full.

Pass --full to re-read everything and recompact, which also picks up anything an incremental run skipped. A source driven by an explicit --jira-jql or --confluence-cql always reads in full, since that query is authoritative and cannot be narrowed to a delta.

whodar refresh

whodar refresh

Re-index every source that has been indexed at least once, reusing the flags it was last indexed with and merging the result. Each index run records its flags in refresh.json; a source read from stdin is not recorded, since a scheduled run could not supply the input.

whodar schedule

whodar schedule --install | --remove | --status

Install a launchd agent that runs whodar refresh every Sunday at 3am, so the graph stays current without remembering to. macOS only; on other systems run whodar refresh from cron. Logs go to ~/Library/Logs/whodar-refresh.log.

whodar connect

Sets up a source interactively: it explains the source, shows how to create the credential, reads and validates it, runs the first index, and prints the export line to save. Credentials are validated in memory and never written to disk. connect needs a terminal; scripts use whodar index.

whodar connect [source]
Flag Default What it does
--status off Report which sources are configured, without prompting.
--forget off Remove the named source's saved credentials from the OS keychain.

With no argument it shows a menu of every source, marked configured or not. With a source (org-csv, codeowners, git, slack, github, jira, confluence, pagerduty, or graph) it sets up just that one.

whodar vault

Encrypts the on-disk index at rest so a stolen disk or a stray backup cannot read your people graph. Encryption turns on whenever a key is configured, through WHODAR_INDEX_KEY (a base64 32-byte key) or WHODAR_INDEX_PASSPHRASE.

whodar vault keygen     # print a fresh key as an export line
whodar vault status     # report the key and whether the index is encrypted
whodar vault encrypt    # encrypt an existing plain index in place
whodar vault decrypt    # rewrite the index back to plain JSON

With a key set, every whodar index write is encrypted and every read decrypts. Reading an encrypted index without the key fails rather than exposing anything. See PRIVACY.md for the full model.

whodar ask

Answers a question from the index.

whodar ask [flags] QUESTION
Flag Default What it does
--mode keyword Resolver: keyword, semantic, or llm.
--limit 5 Maximum results per section.
--provider ollama LLM provider: ollama, anthropic, openai, or gemini.
--model Chat model for llm mode (defaults per provider).
--embed-model Ollama embed model for semantic and llm modes.
--ollama-url localhost Ollama base URL.
--openai-url OpenAI-compatible base URL including the version path, e.g. http://localhost:1234/v1.
--feedback normal How hard votes move ranking: off, low, normal, high.

Modes: keyword needs no model and always works. semantic blends meaning with your exact words, using embeddings built with index --embed, so a paraphrase can find what the words alone would miss without losing the matches the words already had. llm retrieves candidates, then a model re-ranks them and writes a short recommendation; it cannot invent people. The default provider is a local Ollama server. The anthropic (Claude), openai, and gemini providers are cloud models gated by the egress policy: strict refuses them, --policy redacted sends the question and anonymized numbered candidates (people as title, team, and matched terms; channels as matched terms only) and writes the summary locally, and --policy open sends candidates as-is. The openai provider also speaks to any compatible server via --openai-url; a local one, such as LM Studio, needs no policy opt-in, while a remote one needs --policy open. Each result carries a strength from zero to one: query coverage scaled by the weight of the evidence, where an explicit topic is proof, a title slightly less, a passing mention half.

whodar recall

Finds the past conversation where something was worked out, and who was in it. An answer is a pointer, not a transcript: the people, the place, the date, and a link back to the conversation in the tool it happened in. Opening the link uses your own access to that tool.

whodar recall [flags] QUESTION

Results cover only conversations you took part in, so whodar has to know who you are. It takes --me, then WHODAR_ME, then your git email.

Flag Default What it does
--me Who is asking: an email or a source identifier such as slack:U123.
--limit 5 Maximum conversations to return.
--meaning off Match by meaning instead of exact words. Needs an index built with --episodes --embed.
--how off Show how it was worked out, for conversations whodar keeps. Needs a Memory license at index time.
--provider ollama Model provider for --how. Cloud providers need --policy open, because the conversation is sent whole.
--model Chat model that writes the account for --how.
--openai-url OpenAI-compatible base URL.
--embed-model Ollama embed model for --meaning.
--ollama-url localhost Ollama base URL.
--link-horizon-days 0 Warn that links older than this many days may have expired. Zero makes no claim.

Recall is also a Slack command (/whodar recall ... or recall ... in a mention), where it answers only to the person who asked, and an MCP tool (whodar_recall). The web app serves it on localhost only: the serve token gates the server, not one person's history.

whodar near

whodar near PERSON [--limit N]

Rank the people who work nearest PERSON by shared team and channel membership and shared topics. Co-membership is normalized by group size, so a small tight team counts for far more than a broad channel; groups that span most of the org are ignored as administrative; and permission tiers of one group (store-admin, store-write) fold together. PERSON is an email, an id, or an exact name.

whodar search QUERY [--limit N]

Find people and channels whose name, email, title, team, or topic contains QUERY, ranked by how directly they match. This is a direct lookup, so use it when you know what you are looking for; use ask when you want the people who know a subject ranked by expertise.

whodar related TOPIC [--limit N]

Report the subjects related to TOPIC, on two kinds of evidence, and say which is speaking for each.

The stronger is what is worked on together. Two areas altered in the same commit, fixed under one ticket, described on one page, or carried by a single pull request belong to one body of knowledge whoever did the work, which makes this the only thing whodar knows about a subject that does not run through the people holding it. Git, Jira, Confluence, and GitHub all contribute it the same way, so this works for a company whose work lives in tickets and pages rather than in one large repository. The other sources cannot: a chat message or an on-call shift says who was there, not which subjects one piece of work touched.

How much each one contributes depends on whether the people using it say what their work is about. Measured on public instances: an issue tracker where four in five tickets carry two or more labels or components produces a dense graph, and a wiki where almost nothing is labeled produces none at all, while still answering perfectly well about who knows what. Nothing is wrong in the second case. Connections need stated subjects, and a source only has them if somebody stated them.

Only what a piece of work states counts: labels, components, and the paths a commit touched. Titles and prose do not, since pairing the words of a summary would tie a subject to every turn of phrase somebody used near it. Work that sweeps across many areas at once is ignored, because a rename does not make everything it touched one subject, and so are the directory all the changed paths sit under and the language they are written in, which every change touches by construction.

Three things never form a connection. The labels that describe what is being done to a piece of work rather than what it is about, such as the ones GitHub creates in every new repository, are listed and excluded outright: no shape in the graph tells them from a real area, and four different structural measures were tried before the list was written. Only connections are affected, so somebody can still be the person who knows the documentation.

A subject that turns up in more than half of a source's containers, its projects, its repositories, or its wiki spaces, is dropped too. Meaning the same thing everywhere is what a kind of work does and what an area never does. The rule stays quiet when a source has fewer than three containers, since a single repository cannot show a subject staying inside one.

Two words of one name are not two subjects either. Every file under a directory called data_grand_lyon names data, grand, and lyon, which says how one integration is spelled and nothing more. A subject that names a directory of its own counts wherever else it turns up, so energy stays connected to the utility integrations whose names contain it.

And a subject tied to more than a third of everything else is dropped along with every tie to it. Reaching across the whole vocabulary is what describing a kind of work looks like rather than an area, and it makes every subject appear adjacent to every other. This is the same rule as the graph-wide ubiquity check but measured against the vocabulary rather than the people, because the people version cannot see it: on a real issue tracker the label a bot attaches to every ticket with a patch is carried by a sixth of the contributors, nowhere near the share of people that marks scaffolding, while being tied to seventy per cent of every subject there.

The weaker is shared experts: a subject held by the same people is likely the same body of knowledge, and one held by fewer of them is a specialty within it. It fills in where nothing has been seen worked on together, which is often, since most work touches a single area. Read it knowing that any subject one person holds overlaps perfectly with everything else they hold.

Neither needs a taxonomy anyone had to write, and neither needs a model.

whodar risk

whodar risk [PERSON] [--limit N] [--html PATH]

Score knowledge concentration across the graph: the subjects where one or two people hold most of the expertise, so a single departure is visible before it hurts. Name a person for the offboarding view instead: what the record shows only them in.

--html writes a self-contained knowledge-risk brief to PATH rather than printing. The file needs no network, no server, and no whodar to open, so it can be forwarded to somebody who has neither. It leads with the headline counts, lists every scored subject in a sortable and filterable table with a CSV export, reads the same finding back per person as what would leave with them, and states which sources it saw so a reader can judge how much of the company it covered. The headline counts are taken over every scored subject, so capping the table with --limit never shrinks them.

It also reports one-person connections: subjects that only one person has ever done work across. Where those crossings join up, they are reported as the one body of work they are rather than one row at a time, because that is what would leave with the person. The pairing has to be among the strongest ties both subjects have, which is a rank test rather than a floor on the strength: a floor set by eye cuts the real connections along with the noise, since how much of the time two subjects move as one is a tiny number whenever both are also worked on alone. Without it the report fills with true and worthless findings, of the form that one person is the only one who ever touched both documentation and Kubernetes. Both subjects have their own experts, so nothing that counts experts per subject can show this. What rests on one person there is not either subject but the knowledge that the two belong together, and whoever picks one up after they leave has no reason to look at the other.

It also reports joined work: connected bodies of subjects that get worked on together where the same person leads every one. That is a heavier finding than a concentrated subject on its own, and a per-subject list cannot show it. Ten unrelated subjects held by one person are ten small risks; ten that move together and rest on the same person are one large one, because whoever takes the work over has to learn the whole of it at once.

Deterministic arithmetic over the graph, no model needed.

whodar ownership

whodar ownership

Compare declared ownership, from a source of record such as CODEOWNERS, against who actually has the expertise. A mismatch is ownership drift: the file says one person or team owns an area, but the work and the knowledge sit somewhere else.

The drifted areas are split three ways, because they are three different problems: an owner with no recorded work at all, an owner who is active elsewhere but has never worked in the area they own, and an owner who does work there but less than whoever now leads. Only the last is a judgement call. Weight that a source of record assigned does not count as work, or indexing a CODEOWNERS file would make every owner look active in everything they own.

Read the first of those three carefully. A source of record names people by handle and an activity source names them by address, so an owner whose handle was never linked to their commits is indistinguishable from one who has left. Check that bucket against an alias file before believing it.

It leads with the share, not the list, because in every organization measured so far most declared ownership does not match who does the work, and a list on its own reads as a handful of exceptions. An area is only counted as moved when it has moved to somebody who really works on it: weight is discounted by how much that person does everywhere else, so the few people who touch everything do not take ownership of every area at once. The comparison can only speak for what was indexed, so the share is an upper bound on drift rather than a measurement of it.

Areas owned only by teams or automation (a @org/team handle or a bot account) are counted and reported separately: drift compares people, and a group is nobody to have held or lost an area. The drift share is computed over the judged areas only. The exception is a squad the org chart also knows as a team (alerting-squad and a team named Alerting): its members become the owners of record, and the area is judged by whether one of them leads it. Index an org-chart source alongside CODEOWNERS to get this.

whodar assess

Produces a sealed knowledge-risk assessment from local exports: the deliverable for a diligence engagement, built with no tokens and no network.

whodar assess --repo-path ./target-repo --out acme-assessment
whodar assess --repo-path ./svc-a --repo-path ./svc-b --slack-export export.zip
Flag Default What it does
--repo-path Local repository root, repeatable.
--github-repo owner/name whose reviews to read, repeatable.
--github-url github.com API root, for GitHub Enterprise Server.
--github-pages 20 Pages of pull requests per repository, in hundreds.
--slack-export Slack export zip or its unzipped folder.
--org-csv Org chart CSV.
--codeowners CODEOWNERS file or a repo root holding one.
--out whodar-assessment-<date> Output directory.
--top 10 People covered in the departure file.
--git-since-days 730 How far back to read git history.

Pointing it at a GitHub repository with --github-repo reads who reviewed each change, and credits them with the directories that pull request touched rather than the words of its title. That is what surfaces the people who approve work without writing it, and on public repositories it is the difference between trailing a plain commit count and beating it. It needs WHODAR_GITHUB_TOKEN, and it is the only part of an assessment that touches the network.

The index is built fresh in memory and never saved, so an assessment never mixes with your own index. The output directory holds report.html (the knowledge-risk brief), findings.json, ownership.json, departures.json, assessment.loomseal (the sealed finding, verified offline with whodar attest verify), and a README.txt explaining each file.

whodar attest

whodar attest

Produce a LoomSeal evidence bundle for the current knowledge-risk finding: the claim, a digest of the evidence behind it, and an ed25519 signature. Anyone can verify it offline with the loomseal verifier, with no account and nothing sent anywhere, so the finding can be trusted without trusting the machine that made it. The signing key is created once under the data directory and reused.

A licensed install whose license names its sealing key (register it by sending the sealing key from whodar license status when requesting a license) embeds the signed license inside every sealed payload, making the seal provably issued to the organization. Check the licensing chain of any bundle with:

whodar attest verify FILE

That judges the chain only; bundle integrity is the independent verifier's half: loomseal verify FILE. Keys are published at whodar.dev/verify.

whodar identity

whodar identity [PERSON]

List the inferred identity merges: each handle folded into a person, how confident the merge is, and the evidence for it. Joins by shared email or provider id are certain and are not listed. Correct a wrong merge by editing the alias file and re-indexing.

The evidence is a matching name, a mailbox that differs only by punctuation, a domain the handle names, or a name the handle shortens. Somebody committing as [email protected] is the owner written down as frenck, and gjohansson-st is G Johansson: in neither case does the display name or the mailbox say so. A domain shared by more than one person never matches, which keeps the public providers out, and a shortening has to agree on eight characters with a full name, since six is reached by coincidence.

Those two rules linked 23 owners on home-assistant who otherwise read as doing no work at all, and moved 37 owned areas out of the drift count, without anyone writing an alias by hand. Every merge is listed with its evidence, so a wrong one is visible rather than silent.

--unlinked reports the opposite problem, and usually the larger one. A source of record names owners by handle while an activity source records work by address, so an owner whose two were never tied together looks exactly like one who does nothing: every area they own reads as drifted, and their expertise is missing from every answer. The flag lists those owners, the ones owning most first, which is the worklist for an alias file. Groups named as owners are left out, since no alias can tie a group to an address.

whodar archive

Reports and prunes the conversations whodar keeps.

whodar archive status                          # what is kept and how far back
whodar archive prune --older-than-days 365     # forget conversations over a year old
whodar archive prune --content-only            # keep the links, drop the words

prune is the only command that deletes remembered conversations, and it deletes exactly what you name.

whodar license

Reports which features this install is licensed for.

whodar license status

A license is a small signed file. whodar verifies it against a key compiled into the binary and never contacts a server, so a licensed install works offline. Put it at license.json in the data directory, or point WHODAR_LICENSE at it (the file itself or a path to it).

Without one the free tier is in force, and nothing is held back: the people graph, recall, and the whole intelligence layer (risk, ownership, exposure, departure, sealed findings) all run. Unlicensed runs of the intelligence layer are labeled as evaluations, in the output and inside the signed payload of any sealed bundle, so a licensed report and a look at the product cannot be confused. Licenses are a ladder: a risk license removes the evaluation label from the intelligence layer, and a memory license adds the organization's memory on top, keeping the words of conversations on your own machines so an answer can still show how something was solved after the messages are gone. Both are in early access; email [email protected]. An expired license drops to the free tier and leaves every byte already indexed on disk.

whodar feedback

Records, reviews, and clears votes on answers.

whodar feedback record QUESTION (--person ID | --channel NAME) (--helpful | --not-helpful) [--comment TEXT]
whodar feedback list [--query Q | --person ID | --channel NAME]
whodar feedback clear (--query Q | --person ID | --channel NAME | --all)
Flag Applies to What it does
--person all Person identifier from the answer.
--channel all Channel name from the answer.
--query list, clear Match votes for this exact question.
--helpful record The result answered the question.
--not-helpful record The result was wrong for the question.
--comment record Optional note explaining the vote.
--all clear Clear every recorded vote.

By default each net vote multiplies the result's score by 1.25 for that question and its close variants, clamped at three votes either way. Tune it with --feedback off|low|normal|high on ask, serve, and bot: low is a gentle 1.1x capped at two votes, high is 1.5x capped at four, off ignores votes entirely. Votes live in feedback.json under the data directory and survive re-indexing.

whodar feedback bundle

Writes the redacted feedback report a user can choose to hand to whodar's makers. Composing and sending are two different acts by design: this command only writes a file, so every byte can be read before it goes anywhere, and nothing in the binary can send it.

The full schema, so what can appear in the file is known before it is asked for (whodar-feedback-bundle/1):

Field What it holds
schema, note The format name, and the redaction statement itself
version The whodar build that wrote the bundle
votes Counts only: total, helpful, not, on people, on channels
queryShapes Per vote: the question's word count and the verdict
comments The notes typed with votes, verbatim, with times

Queries, names, addresses, and message text never appear: a question asked of whodar is itself a fact about your organization, so only its word count leaves. The one free text is what was typed as feedback, carried because it was written to be read by someone fixing the product.

An organization can pin even this manual path off with "feedback_bundle": "deny" in its policy file; the lock enforces what a promise only asserts. whodar feedback summary shows the same arithmetic in place without writing anything.

whodar forget

Purges one person from everything whodar stores on this machine.

whodar forget [email protected]
whodar forget "Jane Roe" --yes
Flag What it does
--yes Purge without asking.

Their records are removed under every identity they were known by, the alias entries joining those identities are deleted, references to them are stripped from channel membership and org relations, their retained conversation notes are deleted, and conversations where they were the only participant are forgotten. A conversation with other participants keeps its pointer and loses its searchable words. feedback.json is not touched, and re-indexing a source that still contains the person brings them back.

whodar fact

whodar fact record SUBJECT RELATION OBJECT [--detail D] [--source S]
whodar fact list [SUBJECT] [--relation R] [--source S] [--json]
whodar fact forget [SUBJECT [RELATION [OBJECT]]] [--source S]
whodar fact import [FILE|-] [--source S]

Facts state what no crawl can find: which team owns a service, who to escalate to, and above all what a team does not own. Each is a subject, a relation, and an object, labeled with the source that asserted it, and lives in facts.json next to the index so it survives re-indexing. Relations are owned_by, not_owned_by, escalates_to, reports_to, runs_on, and answers_questions_about; an unknown relation is refused. An import with --source replaces that source's facts, so it is the whole of what the source currently asserts, which makes a catalog easy to pipe in with curl ... | jq ... | whodar fact import --source catalog -. Recorded facts also appear alongside whodar ask answers when they mention a term from the question.

whodar directory

whodar directory

List everyone in the index with their team and topics, a read-only inventory of what has been indexed. It answers "who is in here?" without running a query.

whodar status

whodar status

Report the index: when it was last built; how many people, channels, teams, and topics it holds; how many records each source contributed; whether it carries embeddings; whether a key encrypts it at rest; and the license tier.

whodar doctor

whodar doctor

Diagnose configuration and index problems and print the fix for each. It checks that the index loads, is non-empty, and is fresh, whether it carries vectors and is encrypted at rest, which connector credentials are set, and how many declared owners have no work recorded against them. Every problem prints the exact command that resolves it, and doctor exits nonzero when something stops whodar from answering, so it works as a gate in a script.

whodar serve

Runs the local web UI over the same engine.

whodar serve [--addr HOST:PORT] [--mode keyword|semantic|llm]
Flag Default What it does
--addr 127.0.0.1:8765 Address to listen on.
--mode keyword Default resolver.
--provider ollama LLM provider: ollama, anthropic, openai, or gemini.
--model Ollama chat model for llm mode.
--embed-model Ollama embed model.
--ollama-url localhost Ollama base URL.
--openai-url OpenAI-compatible base URL including the version path.
--feedback normal How hard votes move ranking: off, low, normal, high.

Queries are shareable links: /?q=who+owns+billing runs on load. Every result has feedback buttons, except on a public server: there anyone could vote, and votes re-rank the shared index, so a server marked public records no votes and shows no buttons. Connections are bounded (30s read, 90s write, 2min idle) so slow or stalled clients cannot pile up on a shared box.

whodar demo

Serves the web UI over an index built in process, with no credentials and nothing fetched from the network.

whodar demo [--repo DIR] [--big] [--save-index DIR]

With --repo it reads that repository's git history and opens on the exposure view, so the first screen names the directories the work rests on and the ones resting on a single person. Every row can be checked against the same history with git log, which is what makes this the version worth showing somebody.

Without --repo it builds a simulated company across all eight sources, which covers the conversation and ticket sources a git-only run has nothing to say about. Sample data only; it is discarded when the demo stops.

Takes the same flags as serve, plus four of its own.

Flag What it does
--repo Read a real repository instead of the simulation. Repeatable. Reads git only, so the recall view stays off: git records what changed, not what was said.
--max-commits Commits to read per repository with --repo, 4000 by default, which keeps a large repository starting in seconds.
--big Simulate a company of 200 people rather than the small sample, which is what to look at to judge how whodar reads at a real size.
--save-index Write the simulated company to a directory as a real index and exit, so the other commands can be run against it.

whodar mcp

Serves the index to MCP clients over stdio, so agents such as Claude Code and Claude Desktop can ask who knows what mid-conversation. Tools: whodar_ask, whodar_recall, whodar_person, whodar_directory.

whodar mcp [--embed-model name] [--ollama-url url]

Register with claude mcp add whodar -- whodar mcp, or a mcpServers entry in Claude Desktop's config. Semantic mode works when the index was built with --embed and local Ollama is running; keyword needs nothing.

whodar bot

Runs the Slack bot. Mention it, send it a direct message, or use the /whodar slash command; a trailing --llm or --keyword in the text overrides the mode for that answer. Each user gets ten questions a minute, Slack redeliveries are never answered twice, and a dropped connection reconnects with backoff.

whodar bot [--transport socket|events]
Flag Default What it does
--transport socket socket needs no public URL; events serves HTTP.
--addr 127.0.0.1:8766 Address for the events transport.
--mode keyword Default answer mode.
--limit 5 Maximum results per section.
--provider ollama LLM provider: ollama, anthropic, openai, or gemini.
--model Ollama chat model for llm mode.
--embed-model Ollama embed model.
--ollama-url localhost Ollama base URL.
--openai-url OpenAI-compatible base URL including the version path.
--feedback normal How hard votes move ranking: off, low, normal, high.

whodar version

whodar version

Prints the version stamped at build time. Release binaries carry the tag; a plain go build or go install prints dev.

Environment variables

Credentials are read from the OS keychain, when whodar connect saved them there, or from the environment, never from flags, and are never logged. An environment variable always wins over the keychain, so a one-off run can override a stored value.

Variable Used by What it is
WHODAR_SLACK_TOKEN slack source, bot Bot token (xoxb-).
WHODAR_SLACK_APP_TOKEN bot (socket) App-level token (xapp-).
WHODAR_SLACK_SIGNING_SECRET bot (events) Request signature secret.
WHODAR_GITHUB_TOKEN github source Personal access token.
WHODAR_JIRA_URL jira source Site URL, e.g. https://x.atlassian.net.
WHODAR_JIRA_EMAIL jira source Account email for basic auth.
WHODAR_JIRA_TOKEN jira source API token.
WHODAR_CONFLUENCE_URL confluence source Site URL; falls back to WHODAR_JIRA_URL.
WHODAR_CONFLUENCE_EMAIL confluence source Account email; falls back to Jira's.
WHODAR_CONFLUENCE_TOKEN confluence source API token; falls back to Jira's.
WHODAR_PAGERDUTY_TOKEN pagerduty source Read-only API token.
WHODAR_ANTHROPIC_KEY llm anthropic provider Claude API key.
WHODAR_OPENAI_KEY llm openai provider OpenAI-compatible API key.
WHODAR_GEMINI_KEY llm gemini provider Gemini API key.
WHODAR_SERVE_TOKEN serve, demo Bearer token; required to bind beyond localhost.
WHODAR_POLICY_FILE all commands Extra policy file; a locked /etc/whodar/policy.json overrides it.
WHODAR_ME recall, serve Who is asking, so recall covers your own conversations.
WHODAR_LICENSE index, license A license, or the path to one.
WHODAR_INDEX_KEY index encryption Base64 32-byte key that enables at-rest encryption.
WHODAR_INDEX_PASSPHRASE index encryption Passphrase for at-rest encryption; prompted if unset on a terminal.

Identity aliases

Handles that clearly belong to one person join automatically: a handle-only identifier such as codeowners:carol-lee or github:carollee merges with the one person whose name or email local-part flattens to the same string, so Carol Lee, carol-lee, and [email protected] stay one entry. A handle matching nobody, or matching more than one person, stays separate.

An alias file joins the rest, when neither email nor name can. The file maps a canonical identifier to its aliases:

{"[email protected]": ["github:angela-malone", "codeowners:angela-malone"]}

Pass it once with index --aliases; the mapping persists in the index and joins entries indexed before the file existed. Joined identifiers appear in answers under identities. See examples/aliases.json.

What counts as a subject

A subject is something a source stated: a label, a component, a whole directory, a CODEOWNERS path. A directory called data_grand_lyon is one integration, not four, so the words inside a compound name are searchable without being subjects of their own. A word that does name a directory somewhere keeps its standing wherever else it appears. Those are what risk, ownership, and the connections between subjects are computed over.

Text is mined too, so a question asked in somebody's own words still finds the right person even when nothing was ever labeled. Those words stay searchable and are not treated as subjects on their own. A single ordinary word appearing in several sources is not corroboration, it is just a common word: on a real issue tracker read alongside its wiki, that reading promoted seven and a half thousand words to subjects, and they were "appearing", "avoiding", "overkill", and "work". A mined phrase of more than one word does earn its place, since naming two words together is how people name things: state-store, interactive-query, jwt-bearer. It has to read as a name, though. No part of it may be grammar, which is what separates state-store from "should have" and "during rebalance", and no part may start with a digit, which is what a ticket reference looks like.

What a source states is taken as stated. If a tracker labels its tickets kip-1076, that is a subject the project has, and whodar reports it rather than deciding which of your labels are real ones.

Ranking

Keyword scores weight rarer query terms higher, then cap and saturate each term's accumulated weight, so repeating a word in chat all day cannot outrank the person with the explicit topic, title, or ownership signal. People with far more indexed text than average are further discounted for verbosity, though only mildly, for a measured reason: the people who own the most of an organization are also the people who appear in the most of it, and discounting them heavily for that hands their own subjects to whoever passed through once. Removing the discount is worse again, since then whoever appears most often wins everything. Recency decay and feedback votes then scale the result.

How often somebody touched a subject counts one for one at first and logarithmically after that, so an owner with three hundred commits in an area outranks somebody with four, while no single subject can run away with a person's whole profile.

Both of those are tuned against real repositories rather than by taste. The measure is a project's own CODEOWNERS file: index only its commit history, hold the ownership file out, and ask whodar who knows each area. The labels are written by that project's maintainers, so the answer is not graded by us.

A reason reading zwave (topic, not lately) means the person knows the subject but has stopped working on it. Knowing something best is not the same as still being in it: measured against a real repository, the leading expert of two subjects in five had already stopped touching them, and such a lead was less than half as likely to still hold the subject six months on. Sources that cannot say what was recent claim nothing either way.

A term that matches nothing falls back to fuzzy matching: the closest indexed term within one edit (four-letter terms and up) or two edits (seven and up) scores instead, at a penalty per edit so an exact match always outranks a corrected one. A corrected term says so in the reasons and names what it was read as, e.g. terraform (topic, read for "terrafrom"), so a lucky save can be told from a wrong guess.

Recency

Dated records lose half their weight per half-life, 180 days by default, so today's owner outranks one from years ago. Tune with --half-life-days at index time; 0 disables decay. Undated sources (org chart, CODEOWNERS, on-call) describe the present and never decay.

Policy

The egress policy decides what whodar may send to a model. strict (default) permits nothing beyond a local model server; non-local --ollama-url and --openai-url values and the cloud providers count as egress and are refused. redacted permits only the known provider hosts (api.anthropic.com, api.openai.com, generativelanguage.googleapis.com) and strips identifiers: the question goes out as typed, people leave as numbered roles (title, team, matched terms), channels leave as numbered matched terms with no names or topics, the model returns numbers, and the summary is written locally. open sends full candidate detail to any host. The policy does not gate indexing, which talks only to the sources you name with your tokens when you run whodar index, or the bot posting answers back to your Slack workspace.

An organization can also pin "private_channels": "deny" to keep private channels out of the index, and "archive": "deny" to forbid keeping the words of any conversation, whatever the license says.

An organization can pin the policy with a locked file. A locked /etc/whodar/policy.json always wins: WHODAR_POLICY_FILE and --policy are then ignored. The lock constrains the installed binary for regular users; it is not a security boundary against an administrator. Private-channel ingest can be denied the same way. See examples/policy.json.

Files

Everything lives under --data-dir (default ~/.whodar):

File What it holds
index.json The graph, postings, embeddings, aliases, and a capped Slack text sample per person.
feedback.json Votes and the queries behind them, kept apart so they survive re-indexing. Not covered by vault encryption today.
episodes.json Past conversations: who took part, where, when, the link back, and matched terms. Written by whodar index --episodes, and by whodar connect, which records conversations as a matter of course. Holds the words themselves only with a Memory license and --archive. Encrypted with the same key as the index.
license.json The signed license, when one is installed. Verified offline against a key in the binary.
index.sources.json The per-source records the index was built from, redacted to stemmed terms, so a --merge rebuilds without re-reading every source. Encrypted with the same key as the index.
index.state.json Per-source incremental watermarks: the newest activity time indexed for each source and scope, so a --merge fetches only what changed. Plain JSON of timestamps and scope names.