A local RAG system for developer teams that ingests code, GitHub PRs, GitHub issues, Jira Cloud tickets, Slite pages, Slack conversations, documents, and Claude Code session logs — surfaced through Claude Code slash commands and a standalone CLI. Designed for zero-friction adoption: index your repo, search immediately.
Most code search tools treat source files as plain text. DevRAG uses tree-sitter AST parsing to chunk code along semantic boundaries (functions, classes, methods), so searches return meaningful units instead of fragments split mid-function.
It combines vector search (semantic) with BM25 (keyword) via Qdrant's server-side Reciprocal Rank Fusion, which is critical for code where exact identifiers like refreshToken matter as much as the concept of "token refresh." Both signals live in Qdrant as named vectors on the same points (dense + bm25), so search is a single round trip per collection and metadata filters apply equally to both legs.
PR history, issues, Jira tickets, and Slite wiki pages are first-class data sources — encoding why code changed, what bugs and features are tracked, what project management context exists, and what internal knowledge base content says.
Install directly from GitHub (no clone required):
# As a global CLI tool (recommended)
uv tool install git+https://github.com/tomharris/dev-rag.git
# Or run without installing
uvx --from git+https://github.com/tomharris/dev-rag.git devrag --help
# Or add as a project dependency
uv add git+https://github.com/tomharris/dev-rag.git
# pip install git+https://github.com/tomharris/dev-rag.gitFor development (from source):
git clone https://github.com/tomharris/dev-rag.git
cd dev-rag
uv sync
uv run devrag --help# Index the current repository
devrag index repo .# Search across code, PRs, issues, and docs
devrag search "how does authentication work"# Check what's indexed
devrag status- Python 3.12+
- Ollama running locally with
nomic-embed-textpulled:ollama pull nomic-embed-text
DevRAG's reranker and BM25 models come from Hugging Face. On first use it auto-downloads a self-hosted bundle of both models from a dev-rag GitHub release (reachable even when huggingface.co is blocked) and caches them locally; later runs load offline. To fetch them explicitly:
devrag download-models # fetch/refresh the model bundle
devrag download-models --force # re-download even if cachedOverrides (in ~/.config/devrag/devrag.yaml or .devrag.yaml):
network:
auto_download_models: true # set false to disable auto-fetch (CI/air-gapped)model_bundle_url: ""# internal/air-gapped mirror of the bundlemodel_bundle_sha256: ""# expected checksum for a custom model_bundle_urlsparse_embedding:
cache_dir: ""# FastEmbed cache; "" = ~/.cache/devrag/fastembedNote: the bundle carries the default model names. If you override
retrieval.reranker_model or sparse_embedding.model, you need Hugging Face (or
a faithful mirror) access for your chosen model.
devrag search "query"# Search all sources
devrag search "query" --scope code # Code only
devrag search "query" --scope prs # PR history only
devrag search "query" --scope issues # Issues only
devrag search "query" --scope slite # Slite pages only
devrag search "query" --scope slack # Slack conversations only
devrag search "query" --scope docs # Documents only
devrag search "query" --scope sessions # Claude Code session logs only
devrag search "query" --top-k 10 # More results# Narrow with metadata filters (all optional, AND-combined)
devrag search "auth" --repo myrepo --chunk-type diff
devrag search "bug" --pr-number 1234
devrag search "refund flow" --ticket-key PROJ-123
devrag search "deploy" --session-id <uuid>
devrag search "incident" --channel C0123ABCD # Slack channel idThe query router automatically classifies intent and targets relevant collections. "Why did we switch to Redis?" routes to PR history; "is there a bug with login?" routes to issues and Jira tickets; "what sprint is auth in?" routes to Jira; "what does our wiki say about deploys?" routes to Slite pages and documents; "how does the auth middleware work?" routes to code.
# Code (AST-aware, incremental by default; also indexes the repo's docs)
devrag index repo .# Current directory (code + docs)
devrag index repo /path/to/repo # Specific path
devrag index repo . --full # Force full re-index
devrag index repo . --name my-repo # Name for multi-repo support
devrag index repo . --no-docs # Code only, skip the repo's docs
devrag index remove-repo my-repo # Remove a repo from the index (code + docs)
devrag index refresh # Re-index ALL registered repos (incremental; code + docs)
devrag index refresh --full # Full rebuild of every registered repo (external sources untouched)# `index repo` also indexes the repo's own docs (md/mdx/txt/rst/html/adoc) into# the documents collection, tagged with the repo name (so `search --repo <name>`# narrows to them). Use `index docs` below only for a standalone docs tree.# Documents (section-aware splitting; standalone directory, not tied to a repo)
devrag index docs ./specs # Default: **/*.md
devrag index docs ./docs --glob "**/*.txt,**/*.rst"# GitHub PRs (incremental with cursor tracking)export GITHUB_TOKEN=ghp_xxx
devrag index prs owner/repo # Incremental from cursor (90d on first run)
devrag index prs owner/repo --since 180d # Override cursor for backfill# GitHub Issues (incremental, skips PRs)
devrag index issues owner/repo # Last 90 days
devrag index issues owner/repo --since 30d # Custom lookback# Jira Cloud tickets (incremental via JQL)export JIRA_EMAIL=you@company.com
export JIRA_TOKEN=your-api-token
devrag index jira # Uses JQL from .devrag.yaml
devrag index jira --since 180d # Custom lookback# Slite pages (incremental, section-aware chunking)export SLITE_TOKEN=your-api-token
devrag index slite # Last 90 days, all channels
devrag index slite --since 30d # Custom lookback# Slack conversations — NO Slack App required (uses your browser session)export SLACK_XOXC_TOKEN=xoxc-... # web-client token (see "Slack without an app")export SLACK_XOXD_COOKIE=xoxd-... # the `d` cookie value
devrag index slack # Public channels you're in, last 90 days
devrag index slack --since 30d # Custom first-sync lookback# Claude Code session logs (local JSONL, incremental via file mtime)
devrag index sessions # Incremental from cursor (60d first run)
devrag index sessions --since 30d # Override cursor
devrag index sessions --logs-dir /custom/path--since takes a day count only — 90d, 30d, 180d. Anything else (90days, 3w, 6m)
is rejected with a usage error before any indexing work starts.
Every outbound sync (GitHub, Jira, Slite, Slack) retries transient failures automatically:
HTTP 429, 5xx, and connection-level errors (DNS failure, connection refused, reset, timeout).
Backoff honors the server's Retry-After when present — in both the seconds and HTTP-date
forms — otherwise it backs off exponentially with jitter, capped at 60s per wait, for up to
5 attempts.
GitHub's primary rate limit (403 with the quota exhausted) is waited out until the reset
timestamp, and its secondary/abuse limit (429) is honored via Retry-After. A 403 that is
not a rate limit — a token missing a scope, say — fails immediately rather than being retried,
so the real problem surfaces straight away.
Most Slack integrations require creating a Slack App and having a workspace admin install it with OAuth scopes. DevRAG skips that entirely: it authenticates as you, the logged-in user, using the same credentials your browser already holds — so no app, no admin approval, and it indexes exactly what you can already see.
Get the credentials automatically (recommended; you just need to be logged in to the Slack desktop app or a browser):
eval"$(devrag auth slack --workspace mycorp)"# mycorp = the <x> in https://<x>.slack.com
devrag index slackdevrag auth slack reads both credentials from the Slack desktop app by default —
the d cookie from its cookie store and the xoxc token from its localStorage (where
Slack now keeps the token; it's no longer in any web page) — validates the pair, and
prints the two export lines, so eval "$(…)" sets SLACK_XOXC_TOKEN /
SLACK_XOXD_COOKIE in your shell. The desktop app is a Chromium app, so its cookie store
decrypts with the same machinery used for browsers (no extra setup) and its localStorage
is read with a bundled pure-python LevelDB reader — both work even while Slack is running
(the stores are copied first; nothing is written to disk by devrag). Both the direct
download from slack.com and the Mac App Store build are found automatically (the
latter is sandboxed, so it keeps its profile — and its cookie-encryption key — somewhere
else entirely). Set slack.workspace in .devrag.yaml to skip --workspace on re-runs.
--workspace is the subdomain, not the workspace's display name: for a workspace
shown as "Futures Inc" at https://pipeline.slack.com/, pass --workspace pipeline. Get
it wrong and devrag lists the subdomains you're signed in to.
--browser selects the source explicitly: slack (the desktop app) or
chrome/firefox/brave/edge/chromium. With no --browser, devrag tries the Slack
desktop app first, then sweeps every supported browser, skipping any whose profile it
can't read — so an unsupported/missing app or browser doesn't abort the search. The cookie
must live on the machine running the command — if you're logged in only on a different
machine, or your app/browser stores its profile in a non-standard location, use --browser
or the manual fallback below.
Manual fallback — if cookie decryption fails (locked keyring / Chrome app-bound encryption)
Extract the two credentials from your browser (while logged in to Slack on the web):
- Open Slack in your browser → DevTools (F12) → Console.
- Token (
xoxc-…): runJSON.parse(localStorage.localConfig_v2).teams[Object.keys(JSON.parse(localStorage.localConfig_v2).teams)[0]].token(or findxoxc-under Application → Local Storage). - Cookie (
xoxd-…): DevTools → Application → Cookies →https://app.slack.com→ copy the value of the cookie namedd(URL-decode%2F→/etc. if needed). export SLACK_XOXC_TOKEN=xoxc-…andexport SLACK_XOXD_COOKIE=xoxd-…, thendevrag index slack.
Scope: public channels you belong to by default. To index only specific channels
(of any type), set an allowlist in .devrag.yaml under slack.channel_ids. Threads are
indexed as single chunks; loose channel chatter is grouped into time-window chunks.
⚠️ Caveats. These are personal session credentials, not an official API token, and using Slack's internal web API is against a strict reading of Slack's Terms of Service — use at your own risk on workspaces where you have permission to read the content. The token/cookie expire when your browser session rotates or you log out; when that happensdevrag index slackfails with a clear "re-extract from browser" error rather than silently indexing nothing. Credentials are read from env vars only and never stored in config files.
Code indexing is incremental — file content hashes are tracked in SQLite, so unchanged files are skipped on re-index.
devrag status # Show index stats
devrag serve # Start MCP server for Claude Code
devrag reindex --all # Clear all indexes and re-embed code repos + their docs (re-sync PRs/issues/Jira/Slite/Slack manually)
devrag reindex --name my_project # Re-index a single repo's code + docs (preserves other repos and external sources)
devrag config set embedding.model nomic-embed-text
devrag config get vector_store.backendTest retrieval quality with a JSONL file of queries and expected results:
devrag eval run test_queries.jsonl --output results.jsonl --top-k 5
devrag eval compare results_v1.jsonl results_v2.jsonlQuery file format:
{"query": "How does rate limiting work?", "expected_files": ["src/middleware/rate_limit.ts"]}
{"query": "Why did we migrate to Redis?", "expected_prs": [1234, 1301]}Metrics: precision@k, recall@k, and MRR (Mean Reciprocal Rank).
DevRAG ships as a FastMCP server that integrates directly with Claude Code.
# If installed globally via `uv tool install`:
claude mcp add devrag -- devrag serve
# Or run on demand without installing:
claude mcp add devrag -- uvx --from git+https://github.com/tomharris/dev-rag.git devrag serveThe repo includes Claude Code skills in .claude/skills/:
| Skill | Description |
|---|---|
/rag-search <query> | Search code, PRs, and docs with results grouped by source type |
/rag-index [path] | Index a repo, docs directory, or sync PRs |
/rag-pr <query> | Search PR history for why code changed |
rag-first (auto) | Auto-triggers on codebase questions to search DevRAG before Grep/Glob/Explore |
The rag-first skill fires automatically when Claude detects a codebase question — no slash command needed.
RAG search is guidance, not enforcement — see the Search Strategy section of CLAUDE.md for when DevRAG beats keyword search and when Grep is the right tool.
┌─────────────────────────────────────────────────────────────────────┐
│ DEVELOPER INTERFACES │
│ CLI (Typer) MCP Server (FastMCP) │
│ devrag search|index|status Claude Code slash commands │
└────────────┬──────────────────────────────────┬─────────────────────┘
│ │
▼ ▼
┌─────────────────────────────────────────────────────────────────────┐
│ RETRIEVAL LAYER │
│ Query Router ──→ Hybrid Search (server-side dense+BM25 RRF) ──→ Reranker│
│ (intent → collections) (cross-encoder) │
│ └─→ repo-preference boost ──→ dedup per source ──→ final_k │
└────────────┬────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────┐
│ STORAGE LAYER │
│ QdrantStore (HTTP or embedded local path) │
│ points carry named vectors: {dense, bm25 sparse} │
│ SQLite: file hashes, chunk mappings, sync cursors, metrics │
└────────────┬────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────┐
│ INGESTION LAYER │
│ Code PR Issue Jira Slite Doc │
│ Indexer Indexer Indexer Indexer Indexer Indexer │
│ (AST) (GitHub) (GitHub) (Cloud) (REST) (sections) │
│ │
│ Ollama Embedder (nomic-embed-text, 768d) │
└─────────────────────────────────────────────────────────────────────┘
| Collection | Source | Contents |
|---|---|---|
code_chunks | Code indexer | AST-extracted functions, classes, methods |
pr_diffs | PR indexer | PR descriptions and file-level diff hunks |
pr_discussions | PR indexer | Review comments and threads |
issue_descriptions | Issue indexer | Issue titles and bodies |
issue_discussions | Issue indexer | Issue comments |
jira_descriptions | Jira indexer | Jira ticket summaries and descriptions |
jira_discussions | Jira indexer | Jira ticket comments |
slite_pages | Slite indexer | Slite page sections (markdown) |
slack_messages | Slack indexer | Slack threads and time-window conversations |
documents | Doc indexer | Markdown/text sections |
session_logs | Sessions indexer | Claude Code user↔assistant exchanges from local JSONL |
DevRAG uses YAML configuration with two layers (project overrides user):
- User config:
~/.config/devrag/devrag.yaml - Project config:
.devrag.yaml(commit this to share with your team)
embedding:
model: nomic-embed-text # Embedding model nameprovider: ollama # ollama or sentence-transformersollama_url: http://localhost:11434batch_size: 64max_tokens: 8192# Truncation limit for embedding contextvector_store:
qdrant_url: http://localhost:6333 # Qdrant HTTP endpointqdrant_path: ""# optional: local filesystem path for embedded mode (no server needed)embedding_dim: 768# Vector dimensionalityretrieval:
top_k: 20# Candidates before rerankingfinal_k: 5# Results after reranking (dedup runs on the full pool first)rerank: truereranker_model: cross-encoder/ms-marco-MiniLM-L-6-v2reranker_max_length: 512# Cross-encoder token limit; raise for longer chunksmax_per_source: 2# Max results kept per file/PR/issue/etc.repo_boost: 0.15# Soft preference for the cwd repo (fraction of score spread; 0 = off)code:
chunk_max_tokens: 512respect_gitignore: trueexclude_patterns:
- "*.min.js"
- "vendor/**"
- "node_modules/**"
- "*.lock"
- "*.generated.*"prs:
github_token_env: GITHUB_TOKEN # Env var name (token never stored in config)backfill_days: 90include_draft: falsechunk_max_tokens: 512# Max tokens per PR chunkissues:
github_token_env: GITHUB_TOKEN # Reuses same token as PRsbackfill_days: 90chunk_max_tokens: 512include_labels: [] # Only index issues with these labels (empty = all)exclude_labels: ["wontfix"] # Skip issues with these labelsjira:
jira_token_env: JIRA_TOKEN # Env var for API tokenjira_email_env: JIRA_EMAIL # Env var for email (basic auth)instance_url: https://yoursite.atlassian.netjql: "project = DEV"# JQL filter for scoping ticketsbackfill_days: 90chunk_max_tokens: 512slite:
slite_token_env: SLITE_TOKEN # Env var for API tokenchannel_ids: [] # Filter to specific channels (empty = all)backfill_days: 90chunk_max_tokens: 512chunk_overlap_tokens: 50slack:
slack_token_env: SLACK_XOXC_TOKEN # Env var for the xoxc web-client tokenslack_cookie_env: SLACK_XOXD_COOKIE # Env var for the xoxd `d` cookiechannel_ids: [] # Allowlist of channel ids (empty = all public channels you're in)gap_minutes: 30# Quiet gap that starts a new conversation windowbackfill_days: 90chunk_max_tokens: 512chunk_overlap_tokens: 50sessions:
logs_dir: ~/.claude/projects # Claude Code JSONL log directorybackfill_days: 60chunk_max_tokens: 512documents:
glob_patterns: ["**/*.md", "**/*.mdx", "**/*.txt", "**/*.rst", "**/*.html", "**/*.adoc"]chunk_max_tokens: 512chunk_overlap_tokens: 50network:
ca_bundle: ""# PEM CA bundle for all outbound HTTPS (Slack/Jira/Slite/GitHub)If you're behind a TLS-intercepting corporate proxy, devrag auth slack, index prs/issues/jira/slite/slack, and their MCP equivalents may fail with:
[SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: self-signed certificate in certificate chain
httpx verifies against the bundled certifi CA list and does not consult the macOS keychain or honor SSL_CERT_FILE on its own, so the proxy's injected root CA is unknown to it. Point DevRAG at a PEM bundle containing that root CA, either via config:
network:
ca_bundle: ~/corp-ca.pemor via the standard env vars (honored as a fallback when ca_bundle is empty):
export REQUESTS_CA_BUNDLE=~/corp-ca.pem # or SSL_CERT_FILETo export your proxy's root CA from the macOS keychain to a PEM:
security find-certificate -a -p -c "<your proxy CA name>" \
/Library/Keychains/System.keychain >~/corp-ca.pem(or in Keychain Access → select the root CA → File → Export Items → .pem). The Ollama embedder talks to localhost and is unaffected.
DevRAG respects .gitignore by default. For additional exclusions specific to the index, create a .devragignore file. It uses full .gitignore syntax — directory patterns (docs/internal/), anchoring, **, and ! negation all work — and applies to both code and docs indexing. Unlike .gitignore, it can also exclude files that are tracked by git.
DevRAG uses Qdrant as its vector store. You can run it either embedded (local filesystem, no server needed) or against a Qdrant server.
Embedded mode (single-user, no server):
vector_store:
qdrant_path: ~/.local/share/devrag/qdrantServer mode (shared, scales horizontally):
docker run -d -p 6333:6333 -v qdrant_data:/qdrant/storage qdrant/qdrantvector_store:
qdrant_url: http://localhost:6333Switching modes is a config change; re-index with devrag reindex --all after switching.
git clone https://github.com/tomharris/dev-rag.git
cd dev-rag
uv sync --extra dev
# Run tests
uv run pytest tests/
# Run a single test
uv run pytest tests/test_config.py::test_load_config -v
# Integration tests (requires Ollama running with nomic-embed-text)
SKIP_INTEGRATION=0 uv run pytest tests/test_integration.py -vMIT