Configuration
Drop a shire.toml in the repo root to customize behavior:
# Custom database location (default: .shire/index.db)
db_path = "/path/to/custom/index.db"
[discovery]
manifests = ["package.json", "go.mod", "go.work", "Cargo.toml", "pyproject.toml", "pom.xml", "build.gradle", "build.gradle.kts", "settings.gradle", "settings.gradle.kts", "cpanfile", "Gemfile", "flake.nix"]
exclude = ["node_modules", "vendor", "dist", ".build", "target", "third_party", ".shire", ".gradle", "build"]
# Symbol extraction
[symbols]
exclude_extensions = [".proto", ".pl"]
exclude_patterns = [] # file name patterns to skip (suffix match, e.g. "_generated.go"; or prefix, e.g. "zz_generated.")
references_enabled = false # EXPERIMENTAL, default false — see below
max_file_size = 0 # 0 = disabled (default); set to e.g. 2097152 for 2 MiB cap
max_references_per_file = 10000 # 0 = unlimited; default 10000 — caps cross-references per file
include_private = true # default true — index private/unexported symbols; see below
# Documentation indexing
[docs]
extensions = [".md", ".rst", ".txt", ".adoc"]
max_file_size = 262144 # 256 KB — files larger than this are truncated
# MCP server on-demand rebuild
[serve]
debounce_s = 5 # `serve --root` re-checks the working tree at most this often
# Override package descriptions
[[packages]]
name = "legacy-auth"
description = "Deprecated auth service — do not add new dependencies"
File discovery and .gitignore
The manifest walk, the file index, and symbol extraction all skip:
- Directories named in
discovery.exclude - Anything matched by a committed
.gitignoreor.ignorefile, at the repo root or in any nested directory
They deliberately do not honor a developer’s personal global gitignore (git config core.excludesFile) or the untracked, per-clone .git/info/exclude — only files checked into the repo affect what gets indexed, so the index is the same for every collaborator and in CI regardless of local git configuration.
A malformed pattern in a .gitignore (one git accepts but the ignore crate’s glob compiler rejects — e.g. brace alternation, a trailing backslash) is logged as a warning and otherwise ignored; it never aborts a build.
Config precedence
Config is resolved in this order, with no merging — the first one found is used whole, and none of the others are read:
--config <PATH>— explicit path, must exist./shire.toml— repo-root config~/.claude/shire.toml— global config (created byshire init --global)- Built-in defaults
Because the fallback is whole-file replacement rather than a merge, a local shire.toml containing only db_path discards every other setting in ~/.claude/shire.toml (excludes, custom discovery rules, etc.) rather than layering on top of it.
What gets walked
Every shire walk — manifests, files, and source files for symbol extraction —
skips hidden entries, the directories in discovery.exclude, and anything
matched by a committed .gitignore (the repo root’s and any nested ones).
Ignore files that are not part of the repository are deliberately not
consulted: neither your personal global gitignore (core.excludesFile,
usually ~/.gitignore_global) nor the per-clone .git/info/exclude. Both are
machine-local, so honouring them would make the index — and what
search_symbols can find — depend on which machine built it, with no
diagnostic and nothing in the repo to explain the difference.
To keep a path out of the index for everyone, add it to the repo’s
.gitignore or to discovery.exclude.
Watch daemon
[watch]
debounce_ms = 2000 # milliseconds to wait after last change before rebuilding
Logging
[log]
level = "warn" # error, warn, info, debug, trace
dir = ".shire/logs" # log directory (relative to repo root). Set to "" to disable file logging
max_days = 30 # automatically delete log files older than this
The SHIRE_LOG environment variable overrides the config level (e.g., SHIRE_LOG=debug shire build). Log files are daily-rotated with filenames like shire.log.2026-03-26. Each session includes a unique session ID for correlation across concurrent processes.
All fields are optional. Defaults are shown above. The --db CLI flag takes precedence over db_path in config.
Cross-reference index (experimental)
symbols.references_enabled (default false) populates the symbol_refs
table so the symbol_references, symbol_callers, and symbol_callees
MCP tools can answer “where is this used?” / “who calls this?” questions.
Reference extraction is supported for 8 tier-1 languages: Go, Python,
Java, TypeScript, JavaScript, Perl, Ruby, Scala.
Opt-in: shire init asks whether to enable this (prompt labelled
experimental), and writes references_enabled = true to shire.toml
when you say yes. You can also add it manually:
[symbols]
references_enabled = true
Cost: DB grows substantially — roughly +30% on TS/JS repos to +150% on Go-heavy repos (benchmarks on shire-bench: turborepo +29%, grafana +152%, kubernetes +104% vs main baseline). Build time grows ~5-7%.
Toggling the flag takes effect on the next build. Disabling wipes
symbol_refs at the start of the build; re-enabling repopulates it on
the next full rebuild (shire build --force).
This feature is marked experimental: its schema and coverage may change in minor versions as language support broadens and edge cases surface.
Private symbols
symbols.include_private (default true) controls whether private and
unexported symbols are indexed. They are tagged visibility = "private", by
each language’s own convention — a lowercase Go name, a leading _ in
Python, a non-pub Rust item, a private Java member (see the Visibility
column in Supported Ecosystems) — and search_symbols ranks them after
the public ones, so they are there when you look for a helper by name without
crowding out the API.
[symbols]
include_private = false # index only what other code can use
With false, symbols whose visibility is private are dropped at extraction
time. internal and protected symbols are kept either way, and
cross-references (references_enabled) are unaffected — calls made from
inside a private function are still recorded.
Size: private code is often most of a codebase. As a guide, Shire’s own
(Rust) source indexes roughly 3.4x as many symbols with the default as with
include_private = false, and symbols and symbols_fts grow by about that
much. Expect a smaller jump in
code that is mostly exported, and a larger one in application code full of
helpers.
Toggling the option takes effect on the next build, which re-extracts every
source file once (no --force needed). The same happens once after upgrading
to a Shire version whose extractor output changed.
Custom package discovery
For codebases where packages aren’t defined by standard manifest files — Go single-module monorepos, repos that use ownership.yml + build files, or any non-standard convention — you can define custom discovery rules:
# Discover Go apps: directories containing both main.go and ownership.yml
[[discovery.custom]]
name = "go-apps"
kind = "go"
requires = ["main.go", "ownership.yml"]
paths = ["services/", "cmd/"]
exclude = ["testdata", "examples"]
max_depth = 3
name_prefix = "go:"
# Discover proto packages: directories containing *.proto and buf.yaml
[[discovery.custom]]
name = "proto-packages"
kind = "proto"
requires = ["*.proto", "buf.yaml"]
paths = ["proto/", "services/"]
max_depth = 4
| Field | Required | Description |
|---|---|---|
name | yes | Rule identifier |
kind | yes | Package kind for symbol extraction (go, proto, npm, etc.) |
requires | yes | File patterns that must ALL exist in a directory (supports globs like *.proto) |
paths | no | Limit search to specific subtrees (default: repo root) |
exclude | no | Rule-specific directory exclusions (on top of global excludes) |
max_depth | no | Maximum depth to search from each paths entry |
name_prefix | no | Prefix prepended to directory-derived package name (e.g., go:services/auth) |
extensions | no | Override which file extensions get symbol extraction |
Custom discovery runs alongside manifest-based discovery. Directories already found by manifest parsers are skipped. Subdirectories of matched directories are also skipped to prevent nested matches. A manifest package nested under a custom package still owns its own subtree: its files are indexed for it, not for the custom package. Custom packages are re-checked on every incremental build like manifest packages, but one whose directory stops matching its rule is not removed (not even by shire build --force); run shire clean and rebuild to drop it.