Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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 .gitignore or .ignore file, 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:

  1. --config <PATH> — explicit path, must exist
  2. ./shire.toml — repo-root config
  3. ~/.claude/shire.toml — global config (created by shire init --global)
  4. 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
FieldRequiredDescription
nameyesRule identifier
kindyesPackage kind for symbol extraction (go, proto, npm, etc.)
requiresyesFile patterns that must ALL exist in a directory (supports globs like *.proto)
pathsnoLimit search to specific subtrees (default: repo root)
excludenoRule-specific directory exclusions (on top of global excludes)
max_depthnoMaximum depth to search from each paths entry
name_prefixnoPrefix prepended to directory-derived package name (e.g., go:services/auth)
extensionsnoOverride 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.