opencode-zoekt-code-search
v0.2.1
Published
OpenCode plugin that keeps a Zoekt code-search index fresh in the background.
Maintainers
Readme
zoekt-code-search
Reusable agent skill and production-grade OpenCode plugin for super-fast, highly accurate repository-wide code search with Zoekt.
Use this when looking for code. It should be preferred over grep, ripgrep, and find for broad code searches, references, definitions, symbols, file-filtered searches, and repeated searches across the same repository. Zoekt builds a trigram index first, then searches that index instead of repeatedly scanning every file.
Layout
skills/
zoekt-code-search/
SKILL.md
scripts/
install-zoekt.sh
zoekt-index.sh
zoekt-search.sh
test-integration.sh
bin/
src/
config.ts
index.ts
indexer.ts
logger.ts
test/
*.test.tsStandalone Skill Usage
The skills/zoekt-code-search directory remains usable on its own. Use this path if your agent supports skills but not OpenCode plugins.
Prefer this skill over grep, ripgrep, and find for code search tasks after the index exists. Use single-file reads or exact known paths only when the target file is already known.
Copy or vendor skills/zoekt-code-search into an agent-skills directory such as:
.agents/skills/zoekt-code-search/.claude/skills/zoekt-code-search/
Then run searches through the bundled scripts.
Examples:
bash skills/zoekt-code-search/scripts/zoekt-index.sh
bash skills/zoekt-code-search/scripts/zoekt-search.sh "AgentSession"The scripts auto-install zoekt and zoekt-index into the skill-local bin/ directory on first use.
OpenCode Plugin
The OpenCode plugin is a Bun-built TypeScript package that includes the skill's indexing and search capabilities. Use this path if you want OpenCode hooks, plugin state, and built-in tools without installing the standalone skill.
For OpenCode users, zoekt_search is the preferred code-search tool over grep, ripgrep, and find once indexing has completed.
The plugin also teaches OpenCode this preference at runtime. It registers a tool.definition hook that appends guidance to broad search/shell tool descriptions, telling the model to prefer zoekt_search for repository-wide code lookup before grep, ripgrep, find, glob, or shell scans.
It listens for common OpenCode events:
session.idlefile.watcher.updated
The plugin runs trusted installed zoekt-index and zoekt binaries directly. It does not execute repository-local shell scripts. If either binary is not installed, plugin startup throws an error with installation guidance.
Plugin tools:
| Tool | Purpose |
|------|---------|
| zoekt_search | Search the current repository with Zoekt query syntax and return capped JSONL results |
| zoekt_index | Start a background reindex immediately and return indexing state |
| zoekt_status | Return source directory, index directory, current state, and last run timestamps |
Query Syntax
zoekt_search.query accepts Zoekt query language. Multiple expressions separated by spaces are implicit AND; use or, - negation, and parentheses for richer queries.
Zoekt is fast because it uses positional trigram indexes. Regex queries are optimized by extracting literal substrings when possible, so regexes with stable literal anchors are better than fully generic patterns. Every useful query needs at least one positive atom; negations prune existing matches but do not generate results alone. Case defaults to case:auto: lowercase tends to be case-insensitive, while uppercase triggers case-sensitive matching unless overridden. Results are ranked with code-aware signals including atom matches, proximity, word boundaries, filenames, and symbols when available.
Important: Slash-delimited regex syntax (content:/pattern/) does not work. The / character has no special meaning in Zoekt's query parser — slashes are treated as literal characters, not regex delimiters. Always use quoted strings (content:"pattern") or bare field values (regex:pattern). See Known Limitations below.
Common fields:
| Field | Aliases | Values | Meaning | Example |
|-------|---------|--------|---------|---------|
| bare text | | text | Search content and filenames | AgentSession |
| quoted text | | string | Exact text | "exact phrase" |
| content: | c: | string or regex | Search file content | content:"handleRequest" |
| file: | f: | string or regex | Filter paths or filenames | file:\.go$ |
| regex: | | regex | Regex content search | regex:"func\s+.*\(" |
| lang: | l: | text | Filter by detected language | lang:typescript |
| sym: | | text | Search symbols | sym:"Start" |
| case: | c: | yes, no, auto | Control case matching | case:yes content:"HTTPClient" |
| repo: | r: | string or regex | Filter repositories when metadata exists | repo:"github.com/user/project" |
| branch: | b: | text | Filter branches when metadata exists | branch:main |
| archived: | a: | yes or no | Filter archived repositories | archived:no |
| fork: | f: | yes or no | Filter forked repositories | fork:no |
| public: | | yes or no | Filter public repositories | public:yes |
| type: | t: | filematch, filename, file, repo | Control result type | type:filename README |
Operators:
| Syntax | Meaning | Example |
|--------|---------|---------|
| space | Implicit AND | content:test lang:python |
| or | Alternative expressions | lang:go or lang:java |
| - | Negation | Session -file:test |
| (...) | Grouping | content:test (lang:python or lang:javascript) |
type: applies to the whole expression in its current scope, including or clauses. Use parentheses to scope it to one branch, for example (type:repo foo) or bar.
Examples:
AgentSession
"exact phrase"
handleRequest file:\.go$
TODO file:internal/
Session -file:test
case:yes HTTPClient
sym:Start lang:go
content:"exact phrase" (file:\.ts$ or file:\.tsx$)
content:"error.*handler" -lang:javascript
public:yes archived:no fork:no
type:filename file:"README.md"Known Limitations
No slash-delimited regex syntax
Zoekt's query parser does not treat / as a regex delimiter. Writing content:/pattern/ or regex:/pattern/ will include the literal / characters as part of the match pattern, silently producing wrong results (or no results).
| Syntax | What it actually does |
|--------|----------------------|
| content:/foo.*/ | Matches the literal string /foo followed by .* then / |
| content:"foo.*" | Matches foo followed by any characters (correct regex) |
| regex:foo.*bar | Matches foo followed by any characters then bar (correct) |
Always use content:"pattern" (quoted) or regex:pattern (bare) instead.
\w character class can silently fail
Zoekt's trigram index acceleration can silently drop matches when \w or \w+ character classes interact with certain literal prefixes. For example, content:"pub trait \w*Repository" returns zero results even though matching lines exist, while content:"pub trait .*Repository" works correctly.
This is an upstream issue in Zoekt's trigram extraction. As a workaround, prefer .* over \w* or \w+ in regex patterns:
# May silently fail with some literal prefixes:
content:"pub trait \w*Repository"
# Reliable alternative:
content:"pub trait .*Repository"Search Response Format
zoekt_search defaults to format: "json", which decodes Zoekt's raw []byte fields and returns a normalized JSON object:
{
"query": "AgentSession file:\\.ts$",
"indexDir": "/repo/.zoekt",
"resultCount": 1,
"truncated": false,
"results": [
{
"fileName": "src/index.ts",
"repository": "repo-name",
"language": "TypeScript",
"score": 12.34,
"lineMatches": [
{
"lineNumber": 7,
"line": "export const AgentSession = ...",
"before": "optional previous context",
"after": "optional following context",
"fileName": false,
"score": 4.2
}
]
}
]
}Use format: "jsonl" to return raw Zoekt FileMatch JSONL. Raw JSONL mirrors Zoekt's Go structs, including base64-encoded []byte fields such as Line, Before, and After; prefer normalized json for agent consumption.
Runtime behavior:
- Debounces repeated events with
debounceMs, defaulting to30000. - Uses
spawn()withstdio: "ignore"so reindex output is not buffered in memory. - Runs with
cwdset to the indexed worktree/source directory. - Runs from the git repository root as
zoekt-index -index ./.zoekt -ignore_dirs .git .. - Searches from the git repository root as
zoekt -index_dir $git_root/.zoekt $query. - Defaults the index directory to
<repo-root>/.zoekt. The leading dot is required; do not usezoekt/. - Adds
.zoekt/to the repository.gitignoreif it is not already present. - Creates
.zoekt/.gitignorecontaining*so everything inside the local index directory is ignored. - Queues exactly one follow-up run when changes arrive while an index is already running.
- Tracks state as
idle,scheduled,indexing, orerrorforzoekt_statusand tool metadata. - Shows a best-effort OpenCode toast when indexing starts.
- Exposes bounded search results through
zoekt_searchso large result sets do not flood context. - Logs best-effort status messages through
client.app.log().
Install Zoekt First
Copy and paste this before enabling the plugin:
mkdir -p "$HOME/.local/bin"
GOBIN="$HOME/.local/bin" go install github.com/sourcegraph/zoekt/cmd/zoekt-index@latest
GOBIN="$HOME/.local/bin" go install github.com/sourcegraph/zoekt/cmd/zoekt@latest
case ":$PATH:" in
*":$HOME/.local/bin:"*) ;;
*) printf '\nexport PATH="$HOME/.local/bin:$PATH"\n' >> "$HOME/.zshrc" ;;
esacRestart your shell or run:
export PATH="$HOME/.local/bin:$PATH"Install From npm
After publishing, add the package to opencode.json:
{
"plugin": ["opencode-zoekt-code-search"]
}Optional configuration can be supplied with plugin options:
{
"plugin": [
[
"opencode-zoekt-code-search",
{
"debounceMs": 30000,
"timeoutMs": 120000,
"indexDir": "./.zoekt",
"searchMaxBufferBytes": 2000000
}
]
]
}Environment overrides:
| Variable | Meaning |
|----------|---------|
| ZOEKT_REINDEX_DISABLED=1 | Disable the plugin entirely |
| ZOEKT_REINDEX_DEBOUNCE_MS | Debounce window in milliseconds |
| ZOEKT_REINDEX_TIMEOUT_MS | Reindex timeout in milliseconds |
| ZOEKT_INDEX_DIR | Explicit index directory |
| ZOEKT_INDEX_BIN | Explicit path or command name for zoekt-index |
| ZOEKT_BIN | Explicit path or command name for zoekt |
| ZOEKT_SEARCH_MAX_BUFFER_BYTES | Maximum bytes captured during a search |
Copy-Paste Local Install
If you do not want to install from npm, build once and copy the single bundled JavaScript file into a target repo:
bun install
bun run build
mkdir -p /path/to/repo/.opencode/plugins
cp dist/standalone.js /path/to/repo/.opencode/plugins/zoekt-code-search-reindex.jsThe npm entrypoint at dist/index.js keeps @opencode-ai/plugin external for normal package installs. The standalone bundle at dist/standalone.js includes the plugin helper dependency so it is suitable for direct copy-paste installs.
TypeScript Package Shape
The source is split by responsibility:
| File | Purpose |
|------|---------|
| src/index.ts | OpenCode plugin entrypoint and event filtering |
| src/config.ts | Environment/options parsing and Zoekt binary validation |
| src/indexer.ts | Git index directory resolution, debounce, spawn, and rerun control |
| src/logger.ts | Safe OpenCode app logging wrapper |
| src/searcher.ts | Bounded zoekt search execution and index existence checks |
| src/tool-guidance.ts | Runtime tool-description guidance so OpenCode prefers zoekt_search for code lookup |
| src/toast.ts | Best-effort TUI toast notification when indexing starts |
Package entrypoints:
| Export | Target |
|--------|--------|
| opencode-zoekt-code-search | dist/index.js |
| opencode-zoekt-code-search/server | dist/index.js |
The build emits npm and standalone artifacts:
dist/index.js npm entrypoint, keeps @opencode-ai/plugin external
dist/index.d.ts TypeScript declarations for the npm entrypoint
dist/standalone.js self-contained local plugin bundle for copy-paste installsTests
The Bun test suite covers:
- Invalid numeric config fallback behavior.
- Missing Zoekt binary detection.
- Git index directory resolution relative to the source directory.
spawn()options that avoid output buffering and use the source directory ascwd.- Rerun scheduling when file changes arrive while indexing is active.
- Manual timeout handling for spawned index processes.
- Search index detection, query argument construction, normalized JSON response decoding, raw JSONL mode, and bounded result truncation.
- Runtime tool-definition guidance that tells OpenCode to prefer
zoekt_searchfor broad code lookup. - Best-effort OpenCode toast notification when indexing starts.
Development
bun install
bun run typecheck
bun test
bun run build
npm pack --json --ignore-scriptsUseful scripts:
| Script | Purpose |
|--------|---------|
| bun run typecheck | Run strict TypeScript checks without emitting files |
| bun test | Run the unit tests |
| bun run build | Clean dist/, bundle src/index.ts, and emit declarations |
| npm pack --json --ignore-scripts | Verify publishable package contents |
