Getting started with whodar
This guide takes you from nothing to a working setup: build the tool, index a source, and ask "who do I talk to about X" from the terminal or a browser.
In a hurry to wire in one tool? CONNECT.md has a short copy-paste recipe for each source (Slack, GitHub, Jira, Confluence, PagerDuty, git), with the exact credential to create and how to verify it worked. This guide is the fuller walkthrough with the concepts behind it.
What whodar does
You give whodar your work data (an org chart, a Slack workspace) and it builds a local, searchable map of who knows what and which channel to ask in. You then ask a plain-language question and get a ranked list of people and channels, with a short reason for each. It works without any model. If you have a local model through Ollama, it can also write a one-line recommendation.
Everything stays on your machine by default. Nothing is uploaded unless you explicitly change the policy.
Requirements
- Go 1.26 or newer to build from source.
- Optional: a Slack bot token if you want to index Slack.
- Optional: Ollama if you want the LLM answer mode.
Install
Install with Homebrew:
brew tap dcadolph/whodar
brew install whodar
Or build from source, which needs Go 1.26 or newer:
git clone https://github.com/dcadolph/whodar.git
cd whodar
go build -o whodar .
mkdir -p ~/bin && mv whodar ~/bin/
Check it runs:
whodar version
The examples below use whodar. From a source checkout without installing, use
go run . from the repository instead.
Try it in sixty seconds
The fastest look is the demo: a simulated company indexed across all eight sources and served in the web UI, no credentials needed.
whodar demo
It opens a browser on an answered question. Click a name for details, try "who owns terraform", vote on a result. Sample data only; it is discarded when the demo stops.
For the command-line loop, the repository ships a small example org chart:
whodar index --source org-csv --file examples/people.csv
whodar ask --pretty "who do I talk to about billing retries"
You should see a ranked list of people, each with a score and the reason it matched, such as "retries (topic)" or "billing (team)". That is the whole loop: index a source, then ask.
Adding the rest of your tools is the same loop. For a guided setup that validates
the credential and runs the first index, run whodar connect; the sections below
are the manual path.
Index your own org chart
whodar reads a CSV with a header row. Column order does not matter and the header names are matched loosely, so "Job Title" and "role" both map to title. Recognized columns:
| Column | Also accepts | Meaning |
|---|---|---|
| name | full name, employee, person | Display name |
| mail, email address | Used to merge with other sources | |
| title | job title, role | Job title |
| team | department, dept, group | Team name |
| org | organization, division, business unit | Organization name |
| manager | manager email, reports to | Manager identifier |
| topics | skills, tags, expertise | Semicolon-separated topics |
A row needs at least a name or an email. Topics are split on semicolons by default. A minimal file looks like this:
name,email,title,team,topics
Angela Malone,[email protected],Staff Engineer,Payments,billing;retries
Index it:
whodar index --source org-csv --file /path/to/your/people.csv
Index Slack
Slack is the strongest source, because it shows which channels exist, what they are about, and who is active on each topic.
Create a Slack app and token
-
Go to https://api.slack.com/apps and choose Create New App, then From scratch. Pick your workspace.
-
Open OAuth and Permissions. Under Bot Token Scopes add:
channels:read,channels:history,users:read, andusers:read.email. To index private channels as well, also addgroups:readandgroups:history. -
Install the app to the workspace and copy the Bot User OAuth Token. It starts with
xoxb-. -
Export it. whodar reads the token only from the environment, never a flag:
export WHODAR_SLACK_TOKEN=xoxb-your-token
Run the index
whodar index --source slack
By default this reads public channels and the last 180 days of history, capped
at 5000 messages per channel. A bot can only read the history of channels it
has joined, so invite it (/invite @whodar) to the channels that matter;
unreadable channels are skipped with a warning, not fatal. Tune the depth:
whodar index --source slack --since-days 90 --max-messages 2000
To include private channels the token can read:
whodar index --source slack --include-private
What gets stored
Message text is tokenized and kept in the local index only, alongside who posted about what. The token itself is never written to disk and never logged. Nothing is sent to any third party.
Ask questions
whodar ask "who owns billing retries"
Output is JSON on stdout, so you can pipe it into other tools. Add --pretty to
read it yourself. Useful flags:
--limit Ncaps results per section (default 5).--mode keyword(default) ranks with no model.--mode llmadds a local model. See the next section.
The answer has two sections. People are who to talk to. Channels are where to ask, each with its most active members for that topic.
Each keyword-mode result carries a confidence from zero to one: how much of
the question matched, scaled by how strong the match is. An explicit topic
counts as proof, a job title slightly less, a passing mention in chat half.
The web UI and the Slack bot show it as strong, moderate, or weak, so a
least-bad answer never dresses up as a sure one.
Confirm or correct answers
When an answer is right, say so; when it is wrong, say that too. Votes adjust future rankings for that question and its close variants:
whodar feedback record "billing retries" --person [email protected] --helpful
whodar feedback record "billing retries" --channel payments --not-helpful --comment "bot answers there now"
Review or undo what has been taught, and tune how hard votes move ranking:
whodar feedback list --pretty
whodar feedback clear --person [email protected]
whodar ask --feedback high "billing retries"
The web UI has the same buttons on every result. Votes live in
feedback.json next to the index, separate on purpose: re-indexing rebuilds
the graph but keeps what people taught it. Boosted or lowered results say so
in their reasons, and a few votes adjust ranking without ever burying the
underlying evidence.
LLM mode
LLM mode retrieves candidates with the index, then asks a local model to rank them and write a short recommendation. The model only ever sees the retrieved candidates, so it cannot invent a person or a channel.
-
Install Ollama from https://ollama.com and start it.
-
Pull a model:
ollama pull llama3.1 -
Ask:
whodar ask --mode llm "who do I talk to about billing retries" whodar ask --mode llm --model qwen2.5 "where do I ask about kafka"
Ollama runs on your machine, so LLM mode is allowed under the default strict
policy. Pointing --ollama-url at a non-local host counts as leaving the
machine and is refused unless the policy permits it.
Cloud models (Claude, Gemini, OpenAI, and compatible servers)
By default nothing leaves the machine. If you explicitly opt in, llm mode can use a cloud model instead of Ollama:
export WHODAR_ANTHROPIC_KEY=...
whodar ask --mode llm --provider anthropic --policy redacted "who owns billing"
export WHODAR_GEMINI_KEY=...
whodar ask --mode llm --provider gemini --policy redacted "who owns billing"
export WHODAR_OPENAI_KEY=...
whodar ask --mode llm --provider openai --policy redacted "who owns billing"
The policy decides what the model sees. Under --policy redacted, the
question goes out as you typed it, people leave as anonymized numbered roles
(title, team, and matched query terms), and channels leave as numbered
matched terms, with no names, no emails, no channel names, and no message
text. The question is the one part you control: if you type a person's name
into it, that name goes to the model. The model returns numbers, whodar maps
them back, and the summary is written locally. Redacted egress is limited to
the known provider hosts, so a remote --openai-url needs --policy open.
Under --policy open, candidates go as-is. Under the default strict policy,
cloud providers are refused, and a locked org policy can pin that permanently.
The openai provider speaks the common chat-completions format, so
--openai-url also points it at local servers like LM Studio or vLLM; a local
URL needs no policy opt-in at all. Keys are read only from the environment and
are never logged or stored.
Semantic search (Meaning mode in the web UI)
Keyword search matches words. Semantic search matches meaning, so "who handles failed payments" can find the person tagged with "billing retries" even with no shared word. It uses a local embedding model through Ollama.
-
Pull an embedding model:
ollama pull nomic-embed-text -
Build the index with embeddings:
whodar index --source org-csv --file examples/people.csv --embed -
Search by meaning, or let the LLM use semantic retrieval:
whodar ask --mode semantic "who handles failed payments" whodar ask --mode llm "who handles failed payments"
Embeddings are stored in the index. The semantic mode ranks by them directly, and the llm mode uses them to retrieve candidates before the model ranks and summarizes. Set the model with --embed-model. The embedder runs locally, so it is allowed under the strict policy.
Web UI
Prefer a search box to the terminal:
whodar serve
Open http://127.0.0.1:8765, type a question, and pick Keyword, Meaning, or AI. Picking AI reveals a provider choice (local Ollama, Claude, ChatGPT, or Gemini) with live readiness hints saying what each needs. The sidebar also browses everything indexed: people, channels, teams, and topics, each filterable, and clicking a topic asks about it.
Serving with AI enabled
An amber dot on a provider means it needs something; its tooltip says what. The recipes:
Ollama (private, everything stays on this machine, allowed under the default strict policy):
# install from ollama.com, then:
ollama pull llama3.1
whodar serve
Claude, ChatGPT, or Gemini (cloud): export the provider's key and start the server with a policy that permits cloud egress, since the default strict policy keeps everything local:
export WHODAR_ANTHROPIC_KEY=... # or WHODAR_OPENAI_KEY / WHODAR_GEMINI_KEY
whodar serve --policy redacted
Redacted sends the model only your question and anonymized numbered
candidates; --policy open sends full candidate detail. Keys are read only
from the environment and are never typed into the browser. The provider
choice applies per question, and keyword mode keeps working no matter what.
The same flags work on whodar demo if you want to try cloud AI against the
sample company first.
Meaning mode
Meaning mode matches by meaning instead of exact words, so "failed payments"
can find the person tagged "billing retries". It needs the index built once
with --embed; see the Semantic search section below for the two commands. The
server binds to localhost only, so it is not reachable from the network. Stop
it with Ctrl-C; it shuts down cleanly. Change the address with --addr;
binding beyond localhost requires WHODAR_SERVE_TOKEN, and every request
must then carry the token. See docs/DEPLOY.md for the token flow.
Claude Code and other agents (MCP)
Let an agent ask whodar mid-conversation. The MCP server speaks stdio, so registration is one line:
claude mcp add whodar -- whodar mcp
For Claude Desktop, add this under mcpServers in
claude_desktop_config.json:
{"whodar": {"command": "whodar", "args": ["mcp"]}}
The agent gets three tools: whodar_ask (ranked people and channels with
reasons and confidence, keyword or semantic), whodar_person (a full
profile), and whodar_directory (browse people, channels, teams, or
topics). There is no llm mode over MCP on purpose: the calling agent is
already a model, so it reads the ranked candidates itself.
One thing to be clear-eyed about: answers flow to whichever agent you wire this into, and on to that agent's model. Registering the server is the opt-in.
Slack bot
Let your team ask whodar from Slack directly. They mention the bot in a channel
or send it a direct message, and the bot replies in place. Adding --llm to a
message uses the model for that answer, and --keyword forces the fast path.
Scopes and events
In addition to the read scopes from the Slack section, add the bot scopes
chat:write, app_mentions:read, im:history, and im:read. Under Event
Subscriptions, subscribe the bot to app_mention and message.im.
The /whodar slash command
Ask from anywhere without mentioning the bot:
/whodar who owns billing retries
In the app config, open Slash Commands and create /whodar. Over Socket
Mode it just works; the request URL field can hold any placeholder. Over the
Events API, point the request URL at https://your-host/slack/commands; the
same signing secret verifies it. The answer arrives through Slack's response
URL, visible in the channel, and the --llm/--keyword hints work in the
command text too.
Socket Mode (no public URL)
Best for a laptop or an internal host. Enable Socket Mode, create an app-level
token that starts with xapp- with the connections:write scope, then run:
export WHODAR_SLACK_TOKEN=xoxb-...
export WHODAR_SLACK_APP_TOKEN=xapp-...
whodar bot --transport socket
Events API (public endpoint)
Best for a hosted deployment. Point the Slack request URL at https://your-host/slack/events and run:
export WHODAR_SLACK_TOKEN=xoxb-...
export WHODAR_SLACK_SIGNING_SECRET=...
whodar bot --transport events --addr 0.0.0.0:8766
The events transport verifies the Slack request signature and rejects requests with an old timestamp.
Default answer mode
By default the bot answers with the keyword resolver. Set --mode llm to make
the model the default, in which case Ollama must run on the bot's host. Either
way, a user overrides per message with a trailing --llm or --keyword.
GitHub, Jira, and Confluence
Index code, tickets, and wiki pages to learn who works on what.
GitHub needs a token in WHODAR_GITHUB_TOKEN with repository read access:
export WHODAR_GITHUB_TOKEN=ghp-...
whodar index --source github --repo your-org/your-repo
whodar index --source github --github-org your-org --github-emails
It reads contributors, pull request authors, reviewers, assignees, labels and titles, non-pull-request issues, repository topics, and CODEOWNERS, weighted by how much each person works on a topic.
Jira needs a site URL and an API token, created at id.atlassian.com:
export WHODAR_JIRA_URL=https://your-site.atlassian.net
export [email protected]
export WHODAR_JIRA_TOKEN=...
whodar index --source jira --jira-project SEC --jira-project OPS
It reads issue assignees and reporters, weighted by components, labels, summary words, and project. Use --jira-jql for a custom query. When emails are visible, these people merge with Slack and the org chart by email.
Confluence uses the same Atlassian site and token as Jira, so the Jira credentials work, or set WHODAR_CONFLUENCE_URL, WHODAR_CONFLUENCE_EMAIL, and WHODAR_CONFLUENCE_TOKEN:
whodar index --source confluence --confluence-space ENG --confluence-space OPS
It reads page creators and last editors, weighted by labels, title words, and space. Use --confluence-cql for a custom query.
PagerDuty
Index services and on-call schedules to learn who answers for what. Create a read-only API token in PagerDuty and export it:
export WHODAR_PAGERDUTY_TOKEN=...
whodar index --source pagerduty --merge
It reads every service and the people currently on call, giving each on-call person the topics of the services they answer for.
Git history
Index who actually commits to what. No tokens, no API: it reads local clones
directly, so it works for any repository you can git clone, including ones
with no CODEOWNERS file.
whodar index --source git --repo-path ~/src/billing --repo-path ~/src/infra --merge
Each author gets the topics of the paths they touch, weighted by how often
they touch them, so the person doing the work outranks a drive-by. Authors
join other sources by commit email. Bot accounts such as dependabot are
skipped. --git-since-days bounds the window (default 365) and
--max-commits caps each repository (default 2000).
Where your data lives
The index is written to ~/.whodar/index.json by default, readable only by
your user. Override the location with --data-dir. This file holds the
indexed text, so treat it like the source data. It is never uploaded.
Organization policy
The policy governs model egress and is enforced, not advisory. The default is strict: answers are computed locally, and only the keyword resolver and a local model are allowed. Indexing is separate from the policy: it talks only to the sources you name, with your tokens, when you run it.
An organization pins behavior with a policy file at /etc/whodar/policy.json.
When that file sets locked, it wins over both the --policy flag and the
WHODAR_POLICY_FILE environment variable, so a user cannot point whodar at a
looser file. WHODAR_POLICY_FILE remains useful on unmanaged machines and in
tests. See examples/policy.json:
{
"mode": "strict",
"locked": true,
"private_channels": "deny"
}
With the file above, --policy open is ignored and --include-private is
refused. The lock constrains the installed binary for regular users; it is
not a security boundary against an administrator. This is how a cautious
organization keeps a managed install locked down while an individual running
their own copy stays free to opt in.
Updating the index
By default a new run replaces the index, so people who left the org or channels
that went away drop out and new ones appear. To combine sources instead, add
--merge and the run adds onto the existing index. People merge by email, so one
human stays one entry across the org chart, Slack, GitHub, Jira, Confluence,
PagerDuty, and code ownership.
Start with the org chart, then merge every other source onto it:
whodar index --source org-csv --file people.csv
whodar index --source slack --merge
whodar index --source github --github-org your-org --merge
whodar index --source jira --jira-project PROJ --merge
whodar index --source confluence --confluence-space ENG --merge
whodar index --source pagerduty --merge
whodar ask "who knows the billing service"
Each run prints what joined and left since the last index, for example
"+3 people, -1 people, +1 channels". Add --changes-file changes.json to write
the full diff as JSON for a script or a report.
Joining one person across sources
People merge by email. When a source only knows a handle or an account id, such
as a GitHub login without a public email or a CODEOWNERS @handle, the same
human shows up as two entries. An alias file declares which identifiers belong
to the same person:
{
"[email protected]": ["github:alice", "codeowners:alice"]
}
Pass it once with --aliases and the index joins the entries, including ones
indexed before the file existed:
whodar index --source github --github-org your-org --merge --aliases aliases.json
The mapping is saved in the index, so later runs keep joining without the flag.
Joined identifiers appear in answers under identities, and a person's email
always wins as the display identifier. See examples/aliases.json.
Recent activity counts more
Activity ages. Someone who owned a topic three years ago is usually the wrong person to ask today, so dated records decay: a record loses half its weight per half-life, 180 days by default. Slack messages, GitHub pull requests and issues, Jira issues, and Confluence pages all carry their activity date. The org chart, CODEOWNERS, and PagerDuty on-call describe the present and never decay.
Tune it at index time:
whodar index --source slack --merge --half-life-days 90
A shorter half-life favors the people active right now; --half-life-days 0
turns decay off entirely.
Troubleshooting
| Message | Cause | Fix |
|---|---|---|
no index: run whodar index first |
No index built yet | Run a whodar index command |
| invalid arguments: set WHODAR_SLACK_TOKEN | Slack token not exported | export WHODAR_SLACK_TOKEN=xoxb-... |
| private-channel ingest is disabled by policy | Policy denies private channels | Drop --include-private or adjust the policy |
| llm host ...: policy: egress denied | Non-local Ollama under strict | Use a local URL or --policy open |
| llm: request to http://localhost:11434 ... refused | Ollama is not running | Start Ollama and pull a model |
| slack ...: api error: invalid_auth | Bad token or missing scope | Recreate the token with the listed scopes |
| --policy ignored; pinned by org policy | A locked org policy is in effect | Expected. Ask your administrator |
Command reference
whodar index --source org-csv --file FILEbuilds the index from a CSV.whodar index --source slack [--include-private] [--since-days N] [--max-messages N]builds the index from Slack.whodar index --source github (--repo owner/name | --github-org ORG)indexes GitHub.whodar index --source jira (--jira-project KEY | --jira-jql JQL)indexes Jira.whodar index --source confluence (--confluence-space KEY | --confluence-cql CQL)indexes Confluence.whodar index --source pagerdutyindexes PagerDuty services and on-call.whodar index --source git --repo-path DIR [--git-since-days N] [--max-commits N]indexes local git history.whodar demoexplores a simulated company in the web UI, no credentials.whodar index ... --mergeadds the source to the existing index instead of replacing it.whodar ask [--mode keyword|semantic|llm] [--limit N] [--pretty] QUESTIONanswers a question.whodar index ... --embedadds embeddings for semantic and llm retrieval.whodar feedback record QUESTION (--person ID | --channel NAME) (--helpful | --not-helpful) [--comment TEXT]records a vote;whodar feedback listandwhodar feedback clearreview and undo votes.whodar serve [--addr HOST:PORT] [--mode keyword|llm]runs the web UI.whodar bot [--transport socket|events] [--mode keyword|llm] [--addr HOST:PORT]runs the Slack bot.whodar versionprints the version.
Shared flags: --data-dir sets the index location, --policy sets the egress
mode, --pretty indents JSON.