Configuration reference
Every field of searchcode.yaml, with its type, its default and an example.
Three rules apply to the whole file and are worth reading before any single field:
- No secret is ever inline. There is no field to put one in. A credential is
named by a
*Fileor*Envreference and resolved at load, which is what makes this file safe to commit to git and safe to attach to a support ticket. - An unknown key is a startup failure, naming the key and the line. A config
that silently drops a misspelled
authzblock is worse than one that refuses it. - Check a file before you deploy it:
searchcode config validate -f /etc/searchcode/searchcode.yaml. It prints every problem rather than the first, and exits non-zero, so it belongs in CI.
The JSON Schema for the same tree is schema.json; point your editor at it and the fields below autocomplete as you type.
Reading the tables
| Column | Meaning |
|---|---|
| Field | The dotted path. [] marks a list, so repositories[].url is the url of each entry. |
| Type | The YAML type. A duration is a Go duration string — 30s, 2m, 1h — or a bare 0. |
| Default | What the product uses when the key is absent. — means the zero value carries no meaning of its own; required means the key cannot be omitted at all. |
| Example | A real value, taken from the annotated example configuration where it appears there. |
| Reload | reload: a change takes effect without a restart. restart: a reload carrying a change here is rejected, naming the field, rather than half-applied. —: not in either documented list; a change is picked up only where the component re-reads it, so restart to be sure. |
include
Globs, relative to this file's directory, merged over it. Lists append, maps merge, later scalars win. Defaults to conf.d/*.yaml when that directory exists.
| Field | Type | Default | Example | Reload | Description |
|---|---|---|---|---|---|
include |
list of string | — | [conf.d/*.yaml] |
reload | Globs, relative to this file's directory, merged over it. Lists append, maps merge, later scalars win. Defaults to conf.d/*.yaml when that directory exists. |
instance
Where this instance keeps its state.
| Field | Type | Default | Example | Reload | Description |
|---|---|---|---|---|---|
instance.dataDir |
string | — | /var/lib/searchcode |
restart | Clones, the analysis store and caches. Created 0700 at startup. Requires a restart to change. |
instance.diskWarnThresholdPercent |
integer | 80 |
80 |
restart | Warn on /api/v1/status and the admin page at or above this percentage used. Never blocks serving. |
server
Transport-level HTTP settings. Operation timeouts live with the features that own them.
| Field | Type | Default | Example | Reload | Description |
|---|---|---|---|---|---|
server.listen |
string | 0.0.0.0:8080 |
0.0.0.0:8080 |
restart | Address to bind for the API and MCP, host:port. Never put this behind an auth proxy: agents present searchcode-issued bearer credentials. A zero port binds an ephemeral one. Requires a restart to change. |
server.uiListen |
string | 0.0.0.0:8081 |
0.0.0.0:8081 |
restart | Address to bind for the human-facing UI, host:port. Separate from listen so the UI can sit behind an auth proxy while the API and MCP do not. Set to off to run headless: no UI socket is bound and only the API and MCP are served. Empty is an error, not off. Requires a restart to change. |
server.readHeaderTimeout |
duration | 10s |
10s |
restart | Slowloris defence — the timeout that actually matters. How long a client may take to send its headers. |
server.readTimeout |
duration | 30s |
30s |
restart | Whole request including body. |
server.idleTimeout |
duration | 2m0s |
2m0s |
restart | Keep-alive reaping. |
server.writeTimeout |
duration | 0 (unset) |
0 |
restart | Deliberately unset (0). A blanket write deadline breaks streaming MCP and cuts off slow searches over large estates — both would fail as a truncated response rather than as an error. Per-request deadlines are applied in the handlers instead, where the right budget is known. Leave it unset unless a proxy in front of the listener needs a hard bound. |
server.maxHeaderBytes |
integer | 65536 |
65536 |
restart | Go's 1MB default is generous for a service whose largest header is a bearer token. |
server.shutdownTimeout |
duration | 30s |
30s |
restart | How long a graceful shutdown may take to drain in-flight requests. Not one of the five transport timeouts: it budgets an operation, not a connection. |
license
Licence file location. No feature is licence-gated; repository and user counts are the only meters.
| Field | Type | Default | Example | Reload | Description |
|---|---|---|---|---|---|
license.file |
string | — | /etc/searchcode/license.key |
restart | Path to the licence key file. The SEARCHCODE_LICENSE environment variable is an equally supported alternative. |
logging
Structured logging. The level is reloadable.
| Field | Type | Default | Example | Reload | Description |
|---|---|---|---|---|---|
logging.level |
string, one of debug | info | warn | error |
info |
info |
reload | debug | info | warn | error. Reloadable — a level change takes effect without a restart. |
audit
The audit log.
| Field | Type | Default | Example | Reload | Description |
|---|---|---|---|---|---|
audit.output |
string, one of stdout | stderr | file | syslog |
stdout |
stdout |
restart | Where audit records go. stdout needs no writable path beyond dataDir. |
audit.retentionDays |
integer | 400 |
90 |
restart | How long audit records are kept. 0 keeps everything forever. The default of 400 days covers a 13-month audit cycle. |
audit.logSearchQueries |
boolean | true |
true |
restart | Record the text of search queries. Setting this false still leaves usage counts working. |
audit.syslogAddress |
string | — | "…" |
restart | Only with output: syslog. network://address, e.g. udp://10.0.0.1:514. Empty uses the host's local syslog socket. |
auth
How humans authenticate. Exactly one mode, never combined.
| Field | Type | Default | Example | Reload | Description |
|---|---|---|---|---|---|
auth.mode |
string, one of proxy | local | open |
local |
proxy |
restart | proxy | local | open. Exactly one, never combined. NOT the same setting as mcp.auth.mode. Requires a restart to change. |
auth.trustedProxyCIDRs |
list of string | — | [10.0.0.0/8] |
restart | Required in proxy mode: the networks whose identity headers are believed. The instance refuses to start without it. |
auth.sharedSecretFile |
string | — | /run/secrets/proxy-shared-secret |
restart | Required in proxy mode: file holding the secret the proxy sends, proving the request came through it. |
auth.sharedSecretHeader |
string | — | X-Searchcode-Proxy-Secret |
restart | Required in proxy mode: the header carrying the shared secret. |
auth.local.argon2.memoryKiB |
integer | 19456 |
19456 |
restart | Memory cost in KiB. The parameter that actually costs an attacker money. Defaults to 19456 (19 MiB), the OWASP recommendation. Concurrent logins each need this much. |
auth.local.argon2.time |
integer | 2 |
2 |
restart | Number of passes. Defaults to 2. OWASP treats 19456/2, 12288/3, 9216/4 and 7168/5 as equivalent, so trade memory for passes rather than lowering both. |
auth.local.argon2.parallelism |
integer | 1 |
1 |
restart | Number of lanes. Defaults to 1. |
auth.local.loginRatePerMinute |
integer | 10 |
10 |
restart | Login attempts one source address may make per minute. Applied BEFORE the password hash runs, so it bounds what an unauthenticated caller can make this instance spend. Defaults to 10. |
auth.local.initialAdminUsername |
string | — | "…" |
restart | Username created on first boot when an initial password reference is set. Unattended installs only; searchcode user add --admin is the interactive path. There is no default password, and no account exists without one of the two. |
auth.local.initialAdminPasswordFile |
string | — | "…" |
restart | File holding the initial administrator password. A change is forced on first sign-in, because this value persists in the deployment manifest. |
auth.local.initialAdminPasswordEnv |
string | — | "…" |
restart | Environment variable holding the initial administrator password. A change is forced on first sign-in. |
auth.session.insecureCookiesOverPlainHttp |
boolean | false |
false |
restart | SECURITY DOWNGRADE, off by default. Drops the __Host- prefix and the Secure attribute from the session cookie so a browser keeps it over plain http on a LAN address. The session id then travels in clear text and anyone on the network path can steal it. It exists for throwaway trials on a private network; put TLS in front of the UI listener for anything else. Requires a restart to change, and the instance announces it loudly for as long as it is on. |
authz
Authorisation policy: allow-only, no deny rules, order-independent.
| Field | Type | Default | Example | Reload | Description |
|---|---|---|---|---|---|
authz.defaultAccess |
string, one of allow | deny |
deny |
deny |
reload | What applies when no binding matches. Fail closed: deny. |
authz.anonymousRole |
string | — | viewer |
reload | The role an unauthenticated caller gets. Valid only with auth.mode: open. Defaults to viewer, never admin. |
authz.adminGroups |
list of string | — | [platform-admins] |
reload | Instance-scoped, not repository-scoped: full administrative access. |
authz.auditorGroups |
list of string | — | [security-eng] |
reload | Instance-scoped: view_audit_log and view_analytics. |
authz.adminUsers |
list of string | — | [ops.lead@example.com] |
reload | People, by email address, granted exactly what adminGroups grants. auth.mode proxy only; matched against X-Forwarded-Email, case-insensitively for ASCII and literally otherwise. No wildcards. |
authz.auditorUsers |
list of string | — | [compliance@example.com] |
reload | People, by email address, granted exactly what auditorGroups grants. auth.mode proxy only. No wildcards. |
authz.roles |
map of list of string | — | {releng: …} |
reload | Custom roles, from the closed repo-scoped permission set. Instance-scoped permissions come from adminGroups and auditorGroups instead. |
authz.bindings |
list of object | — | […] |
reload | Grants. Order-independent — a principal gets the union of every binding that matches. |
authz.bindings[].name |
string | required | Engineers read platform and open source |
reload | What this grant is for. Shown in the admin UI and in audit records. |
authz.bindings[].groups |
list of string | — | [engineering] |
reload | Identity groups this applies to. "*" means any authenticated principal. A binding needs groups, users, or both. |
authz.bindings[].users |
list of string | — | […] |
reload | People this applies to, by email address, matched against X-Forwarded-Email. auth.mode proxy only. No wildcards: restrict the domain with the proxy's email_domains and use groups: ["*"] instead. |
authz.bindings[].repoGroups |
list of string | — | [platform, open-source] |
reload | Repository groups granted. Globs are allowed on this side only. A binding matching nothing is a typo and fails validation. |
authz.bindings[].role |
string | — | viewer |
reload | A built-in or custom role. |
repositoryGroups
The unit authorisation is granted over. Membership is always explicit, never globbed.
| Field | Type | Default | Example | Reload | Description |
|---|---|---|---|---|---|
repositoryGroups |
list of object | — | […] |
reload | The unit authorisation is granted over. Membership is always explicit, never globbed. |
repositoryGroups[].name |
string | — | payments |
reload | The identifier used in repositories[].groups and in bindings. |
repositoryGroups[].description |
string | — | Payments platform — restricted |
reload | Shown in the admin UI. Free text. |
credentials
Credential references used to reach repositories. No secret is ever inline.
| Field | Type | Default | Example | Reload | Description |
|---|---|---|---|---|---|
credentials |
list of object | — | […] |
reload | Credential references used to reach repositories. No secret is ever inline. |
credentials[].name |
string | required | github-ssh |
reload | Referenced by repositories[].credential. |
credentials[].type |
string, one of ssh | https |
required | ssh |
reload | ssh | https. |
credentials[].privateKeyFile |
string | — | /run/secrets/id_ed25519 |
reload | ssh only: the private key. A path, never a key. |
credentials[].knownHostsFile |
string | — | /etc/searchcode/known_hosts |
reload | ssh only: the known_hosts file to verify the host against. |
credentials[].username |
string | — | "…" |
reload | https only: the username. Not a secret, so it is inline. |
credentials[].passwordEnv |
string | — | "…" |
reload | https only: the environment variable holding the password or token. |
credentials[].passwordFile |
string | — | "…" |
reload | https only: the file holding the password or token. |
repositories
Git repositories to ingest. Usually lives in conf.d/, generated by 'searchcode discover'.
| Field | Type | Default | Example | Reload | Description |
|---|---|---|---|---|---|
repositories |
list of object | — | […] |
reload | Git repositories to ingest. Usually lives in conf.d/, generated by 'searchcode discover'. |
repositories[].url |
string | required | https://github.com/acme/billing |
reload | Clone URL, or an absolute path for a local repository. |
repositories[].groups |
list of string | required | [payments] |
reload | At least one, and each must be a declared repositoryGroup. An unclassified repository is an error, not a warning. |
repositories[].labels |
list of string | — | [tier1, pci] |
reload | Categorisation and facets only. A label grants nothing. |
repositories[].credential |
string | — | github-ssh |
reload | A declared credential name. Omit for a public or local repository. |
repositories[].fullHistory |
boolean | — | true |
reload | Default true. False costs blame, history and coupling analysis for this repository. |
repositories[].depth |
integer | — | 10 |
reload | Clone depth, valid only with fullHistory: false. The escape hatch for a fifteen-year monorepo with binary assets in its history. |
repositories[].refreshInterval |
duration | — | 30s |
reload | How often this repository is fetched. Overrides indexing.refreshInterval. Jittered, so a large estate does not fetch on the hour together. |
repositories[].exclude |
list of string | — | […] |
reload | Path globs never ingested from this repository, layered over indexing.exclude and the built-in exclusion set. A leading ! un-excludes either of those — "!node_modules" is how the one repository whose vendored tree IS the code gets it back. |
directorySources
Plain directories to ingest. The sharpest security boundary in the product.
| Field | Type | Default | Example | Reload | Description |
|---|---|---|---|---|---|
directorySources.allowedRoots |
list of string | — | [/mnt/depot] |
reload | Hard allowlist. Every source path must resolve inside one of these. Non-empty if any source is declared. |
directorySources.denyPaths |
list of string | — | [/mnt/depot/.creds] |
reload | Always refused, even inside an allowedRoots entry. instance.dataDir, this file's own directory and /run/secrets are denied implicitly whether or not they are listed. |
directorySources.sources |
list of object | — | […] |
reload | The directories to ingest. |
directorySources.sources[].path |
string | — | /mnt/depot/cobol |
reload | Must resolve inside an allowedRoots entry. Absolute: a relative path is refused rather than resolved against whatever directory the process happens to be started from. |
directorySources.sources[].groups |
list of string | — | [platform] |
reload | At least one declared repositoryGroup, exactly as for a repository. |
directorySources.sources[].unitDetection |
string, one of single | subdirectories | marker |
— | subdirectories |
reload | single | subdirectories | marker. How one directory becomes one or many searchable units. Defaults to marker. This is a metering input: it decides whether a mount of 2,000 projects counts as one repository or 2,000. |
directorySources.sources[].markerFiles |
list of string | — | […] |
reload | Extra project markers, layered over the built-in set (.git, go.mod, pom.xml, package.json, Cargo.toml). |
directorySources.sources[].maxDepth |
integer | — | 4 |
reload | Bounds the marker search, so a deep tree does not turn unit detection into a full walk. Defaults to 4. |
directorySources.sources[].changeDetection |
string, one of mtime | hash | always |
— | mtime |
reload | mtime | hash | always. How a re-scan decides a file changed. mtime costs one stat per file and is right for most sync tooling. Use hash when the export tooling rewrites every mtime on every run, which mainframe and some Perforce export scripts do; it costs a full content read per scan. always re-reads every unit in full on every refresh interval whether or not anything changed, at the same read cost as hash — it is the last resort, not the safe default, and the instance warns at startup when it is set. It does not re-run the deep analysis pass over bytes the re-read proved identical: that pass is served from its cache, keyed on a digest of every file's content and never on a modification time, so nothing rests on the signal you chose this mode to stop trusting. |
directorySources.sources[].followSymlinks |
boolean | — | false |
reload | A symlink escaping an allowedRoots entry is refused either way. False leaves symlinks unfollowed entirely. |
directorySources.sources[].exclude |
list of string | — | […] |
reload | Path globs never ingested from this source, layered over indexing.exclude exactly as a repository's are. |
discovery
Code-host enumeration, read by the 'discover' CLI only. The server never calls a host.
| Field | Type | Default | Example | Reload | Description |
|---|---|---|---|---|---|
discovery.provider |
string, one of github | gitlab |
— | github |
— | github | gitlab. Used by 'searchcode discover' only — the CLI, never the server. |
discovery.baseUrl |
string | — | https://github.example.com/api/v3 |
— | For GHES or self-managed GitLab. Omit for the public host. |
connections
Code-host connections: the ONE block that can make this process connect to a code host. Absent, it connects to nothing.
| Field | Type | Default | Example | Reload | Description |
|---|---|---|---|---|---|
connections.egress |
string, one of any | internal-only |
any |
any |
restart | any | internal-only. internal-only refuses a connection naming a public code host (github.com, gitlab.com, …) AT CONFIG LOAD, so a firewalled instance has a switch to point at rather than a policy to review. Requires a restart to change. |
connections.keyFile |
string | — | /run/secrets/connections.key |
restart | A file holding the key that encrypts tokens pasted into the UI or 'searchcode setup'. Generate one with 'openssl rand -base64 32'. With neither this nor keyEnv set, connections can only be declared under sources with a secret reference — creating one from a form is refused, and says so. |
connections.keyEnv |
string | — | "…" |
restart | An environment variable holding the same key. Use one of keyFile or keyEnv, never both. |
connections.sources |
list of object | — | […] |
reload | The connections themselves. Absent or empty, this process connects to no code host at all. |
connections.sources[].name |
string | required | acme-github |
reload | Stable identity. Per-connection state hangs off it, so renaming is a delete plus a create, not a rename. |
connections.sources[].type |
string, one of github | gitlab |
required | github |
reload | github | gitlab. Both ship together; the provider for each arrives with its own plan. |
connections.sources[].url |
string | required | https://github.com |
reload | The one host this connection may reach — https://github.com, or your GitHub Enterprise Server or self-managed GitLab. Never derived from a response. |
connections.sources[].token |
object | required | {env: …} |
reload | REQUIRED on every connection, public organisations included — a metadata-scoped, read-only token. GitHub's and GitLab's listing APIs are not usefully anonymous, so enumeration needs one even where cloning does not. A secret reference — {env: NAME} or {file: /path} — never an inline value. This only LISTS; see credential to CLONE. |
connections.sources[].token.file |
string | — | "…" |
restart | Path to a file holding the secret. Relative paths resolve against this configuration file's directory. Re-read on every load, so rotating a mounted Kubernetes Secret needs no restart. |
connections.sources[].token.env |
string | — | ACME_GITHUB_TOKEN |
restart | Name of an environment variable holding the secret. The variable's NAME, never its value. |
connections.sources[].credential |
string | — | gitlab-https |
reload | A declared credential name, used to CLONE what this connection discovers. Without it the clone goes out ANONYMOUS and every private repository is refused, whatever the token above can do — so a private estate needs this as well, on every host. What differs by host is only which SCOPE that credential needs: on GitHub a second, contents-scoped token; on GitLab the same read_api token can serve. Omit for a public estate, which clones anonymously; the token above is required either way. |
connections.sources[].include |
object | required | {orgs, self, users: …} |
reload | What to enumerate. Required — there is no 'everything this token can see' mode, because a broadly scoped token must not become a broad estate by default. |
connections.sources[].include.orgs |
list of string | — | [acme, acme-labs] |
reload | GitHub organisations, or GitLab group paths — a GitLab path nests, so acme/platform/infra is one entry and its subgroups come with it. |
connections.sources[].include.users |
list of string | — | [ben] |
reload | Other users' accounts, or GitLab personal namespaces. Public repositories only, on GitHub. |
connections.sources[].include.self |
boolean | — | false |
reload | The token's own account, including private repositories it can see. Needs no name, which makes 'paste a token' a complete configuration. |
connections.sources[].exclude |
list of string | — | [*-archive] |
reload | Globs over owner/name, applied last so they always win, exactly as 'searchcode discover' applies them. |
connections.sources[].skipArchived |
boolean | — | true |
reload | Skip archived repositories. Defaults to false — an archived repository is enumerated and indexed like any other. |
connections.sources[].skipForks |
boolean | — | true |
reload | Skip forks. Defaults to false — a fork is enumerated and indexed like any other. |
connections.sources[].groups |
list of string | — | [engineering] |
reload | repositoryGroups every repository this connection discovers belongs to. Each must be declared under repositoryGroups. |
connections.sources[].syncInterval |
duration | — | 1h0m0s |
reload | How often this connection re-enumerates. Sync itself is the connection scheduler's; a connection with no scheduler enumerates nothing. |
indexing
The clone and refresh pool.
| Field | Type | Default | Example | Reload | Description |
|---|---|---|---|---|---|
indexing.cloneConcurrency |
integer | 4 |
4 |
reload | Counting semaphore over concurrent clones. |
indexing.window |
string | — | 22:00-06:00 |
reload | Optional "HH:MM-HH:MM". Only starts new work inside it; work in flight is never cut off. |
indexing.maxRepoSizeBytes |
integer | 5368709120 (5 GiB) |
5368709120 |
reload | Checked during clone, not after — a repository over the limit is abandoned rather than cloned then deleted. |
indexing.metricsPerRepo |
boolean | true |
true |
reload | Per-repository metric series. Costs about 2 series and 200-400 bytes of scrape per repository (measured; the range is how long your repository names are), so set false above roughly 2,000 repositories, where a scrape passes half a megabyte and Prometheus stores 4,000 series for this instance alone. It is NOT a fix for a slow scrape: the numbers are read whether or not the series are collapsed. False keeps the repo label and collapses onto repo="all" rather than dropping it, so a dashboard query that groups by it still parses. |
indexing.exclude |
list of string | — | [examples/**] |
reload | Path globs never ingested, layered OVER a built-in set that is always in force — node_modules, vendor, build output, virtualenvs and tool caches, plus whatever pip's own metadata says "pip install -t" put into a repository. Setting this adds to that set rather than replacing it; a leading ! takes one back ("!node_modules", or "!pip-installed" for the pip rule). Every exclusion is counted and reported, never silent: the built-in set's share is files_skipped{reason="excluded_default"} and yours is reason="excluded". |
indexing.refreshInterval |
duration | 1h0m0s |
1h0m0s |
reload | How often a repository is fetched, unless it sets its own. Jittered per repository so two thousand repositories do not all fetch on the hour. |
indexing.minFreeDiskBytes |
integer | 1073741824 (1 GiB) |
1073741824 |
reload | Ingestion pauses while dataDir has less than this much free, and resumes on its own when space returns. Serving is never affected. |
indexing.retryBackoff |
duration | 1m0s |
1m0s |
reload | Pause after the first failed clone or fetch. Doubles per consecutive failure, capped at retryBackoffMax. The last good state keeps serving throughout. |
indexing.retryBackoffMax |
duration | 1h0m0s |
1h0m0s |
reload | Ceiling on the exponential backoff, so an unreachable host is retried on a fixed cadence rather than never again. |
analysis
The analysis pass. Deliberately a separate pool from indexing.
| Field | Type | Default | Example | Reload | Description |
|---|---|---|---|---|---|
analysis.inventoryConcurrency |
integer | 8 |
8 |
restart | Phase A (line counts and languages). Outranks every repository's phase B. |
analysis.deepConcurrency |
integer | 4 |
4 |
restart | Phase B (security, quality, technology). |
analysis.fullResyncInterval |
duration | 168h0m0s |
168h0m0s |
restart | How often the whole estate is re-analysed from scratch. |
analysis.perFileTimeout |
duration | 10s |
10s |
restart | Watchdog on a single file. A file that exceeds it is reported as a gap, never silently skipped. |
analysis.maxEdgesPerRepo |
integer | 500000 |
500000 |
restart | On breach: keep counts, drop edges, report a gap. |
analysis.niceValue |
integer | 10 |
10 |
restart | Scheduling priority of the analysis workers. |
analysis.skipMinified |
boolean | true |
true |
restart | Skip a file whose longest line is over 5,000 bytes BEFORE parsing it, rather than letting it burn the whole perFileTimeout. Defaults to true. The skip is counted and reported with reason 'minified', never silent. |
analysis.history.churnDays |
integer | 90 |
90 |
restart | Default hotspot window in days. 0 disables churn and ranks by complexity alone, which is stated in the response. A caller may pass its own days parameter, capped at 365; this instance default is not capped, because how far back an estate is worth reading is the operator's decision. |
analysis.history.maxAgeDays |
integer | — | 0 |
restart | How far back the walk itself goes. 0 walks all history, bounded by maxCommits — coupling is a question about a repository's whole life, so the default does not window it. |
analysis.history.maxCommits |
integer | 5000 |
5000 |
restart | Commit cap for one walk. A repository with more commits than this is answered from the newest maxCommits, and the truncation is stated in the response rather than applied silently. |
analysis.history.timeout |
duration | 30s |
30s |
restart | Budget for one walk. Overrun is NOT fatal: what was read is kept and the response says the history behind it is partial. |
analysis.history.bulkCommitFiles |
integer | 50 |
50 |
restart | Commits touching more than this many files are excluded from coupling, and the exclusion is stated in the response. Without it, one reformat commit couples every file to every other and the whole feature is worthless. |
analysis.history.minSupport |
integer | 2 |
2 |
restart | Default floor under how many commits a coupled pair must share. One shared commit is a coincidence — most obviously the initial import. |
analysis.history.minConfidence |
number | 0.3 |
0.3 |
restart | Default floor under the conditional probability that changing A also changed B, between 0 and 1. |
search
Search execution budget.
| Field | Type | Default | Example | Reload | Description |
|---|---|---|---|---|---|
search.timeout |
duration | 30s |
30s |
reload | Covers queue time PLUS execution, not execution alone — the number an operator cares about is how long a user waits. |
search.concurrency |
integer | 4 |
4 |
reload | A semaphore that serialises. There is no queue limit and no rejection: a slow search is better than a refused one. |
search.ranking.ranker |
string, one of bm25 | tfidf | simple | structural |
bm25 |
bm25 |
reload | bm25 | tfidf | simple | structural. The scoring model itself. Defaults to bm25. A code_filter forces structural regardless, because the filter's meaning is a region weighting the other three models do not have. |
search.ranking.docPenalty |
number | — | 0 |
reload | 0 to 1. Demotes prose files — Markdown, reStructuredText, plain text — and SCALES WITH TERM COUNT: a one-term query gets no penalty at all, because a README hit is frequently the right answer to one word. 0 disables it. |
search.ranking.commentWeight |
number | 1 |
1 |
reload | 0 to 1. What a match inside a comment is worth against a match in code. 1 disables it. This is the knob aimed at comments mentioning a thing outranking the code implementing it. |
search.ranking.stringWeight |
number | 1 |
1 |
reload | 0 to 1. The same for a match inside a string literal. 1 disables it, which is the default: a string literal is frequently the answer. |
search.ranking.declarationBoost |
number | 1.6 |
1.6 |
reload | 1 to 10. Multiplies a file that DECLARES a query term over one that merely uses it. 1 disables it. Term frequency cannot tell a one-line header declaration from eight call sites, and gets them backwards. |
search.ranking.minTermMatchRatio |
number | 1 |
1 |
reload | 0 to 1. The fraction of a query's terms a file must contain. 1 is AND and is the default. Below 1 this changes WHAT MATCHES rather than what ranks: a saved query starts returning different files. Only a plain word-word-word query is relaxed; anything with NOT, a regex, an OR or a lang: filter is left alone. |
search.ranking.relaxFromTerms |
integer | 3 |
3 |
reload | Term count at which minTermMatchRatio starts to apply. Defaults to 3 — relaxing two terms to one is a different query, not a relaxation. |
search.index.backend |
string, one of cs | caisson |
caisson |
caisson |
reload | cs | caisson. cs brute forces every repository. caisson consults the per-repository index where one exists and falls back to cs where one does not. Defaults to caisson: on a measured 358-repository estate it answered a query matching one file in 5ms against brute force's 887ms, and never more slowly — a query matching every file in the estate, where an index can narrow nothing, came out level. Set cs to turn every index off; what was already built is retained, so reverting a revert costs no re-warm. |
search.index.minFiles |
integer | 1 |
1 |
reload | Searchable files a repository must have before an index is built for it. Defaults to 1 — every repository holding anything gets an index. The obvious per-repository reasoning for a high threshold is sound and its aggregate is not: brute force costs about 2.5ms PER REPOSITORY before it matches anything, so several hundred repositories that are each individually fast cost most of a second to skip, and they are simultaneously the cheapest ones to index because they hold almost no documents. RAM is the reason to raise it: a repository costs bloomSize*8 bytes per 64 documents with one block as the floor, so 32KB each at the default bloomSize whatever their size, and a full estate of 358 repositories measured 24.7MB. Re-evaluated on every refresh AND on every config reload, so lowering it warms the estate without a restart and raising it releases the memory. |
search.index.bloomSize |
integer | 8192 |
8192 |
reload | Filter length in 64-bit words per block of 64 documents, which fixes memory at bloomSize/8 bytes per document — 1024 at the default of 8192. This is the number that governs an estate's RAM, and RAM is the binding constraint. Changing it rebuilds every index. Raised from 4096 on measurement (docs/verification/bloom-sweep.md): false positives come almost entirely from the small tail of files whose filter is over 90% full, and doubling the filter empties that tail — a literal absent from the corpus went from 72 candidate reads to 9. The cost is exactly linear in memory and there is no latency cost, because the scan runs once per block of 64 documents rather than once per word. |
search.index.buildConcurrency |
integer | 2 |
2 |
reload | How many repositories are indexed at once in the background. Deliberately below the search concurrency: warming must never compete with serving, because cs answers correctly throughout and the only cost of warming slowly is that it finishes later. |
search.symbols.backend |
string, one of parsed | heuristic |
parsed |
parsed |
reload | parsed | heuristic. parsed serves the outline from the declarations the analysis pass extracted with a tree-sitter grammar — exact, with a real kind — and falls back to the heuristic for any language this build carries no grammar for. heuristic is the pre-plan-41 behaviour everywhere, including its coverage gaps. Defaults to parsed. |
search.symbols.crossRepoCeiling |
integer | 1200 |
1200 |
reload | How many candidate FILES an estate-wide definitions or references lookup will answer before it DECLINES and says so. It bounds the ANSWER and not the search: above it the query returns no locations plus a gap naming the count, because a name matched in thousands of files cannot be resolved to one answer by matching it and fifty rows out of four thousand look like an answer without being one. Defaults to 1200, from the measurement in docs/verification/41b-01-ceiling.md — on a real 31-repository estate the ubiquitous names (Error 5332 files, New 3774, Validate 2969, Config 1727) are all above it, the names people navigate to (Handler 481, ProcessConstants 28) are all below it, and nothing lands in between. Raising it will NOT make cross-repository navigation faster: the latency is the search, and search.index is the knob for that. There is no unlimited. |
api
The HTTP API.
| Field | Type | Default | Example | Reload | Description |
|---|---|---|---|---|---|
api.rawContentOrigin |
string | — | "…" |
restart | Serve raw repository bytes from a separate origin, so hostile file content cannot script against the UI origin. Empty by default. |
api.apiKeys.ttl.min |
duration | 1h0m0s |
1h0m0s |
restart | Shortest lifetime a key may be issued with. |
api.apiKeys.ttl.default |
duration | 720h0m0s |
720h0m0s |
restart | Lifetime when the caller asks for none. |
api.apiKeys.ttl.max |
duration | 2160h0m0s |
2160h0m0s |
restart | Longest lifetime a key may be issued with. |
api.apiKeys.requireName |
boolean | true |
true |
restart | Refuse a key created without a client name. Defaults to true: an unnamed key is unauditable, and naming it is what lets the audit log say WHICH agent acted. |
api.apiKeys.allowNoExpiry |
boolean | false |
false |
restart | Permit a key that never expires. False by default, because a non-expiring credential is a finding in any audit. |
api.apiKeys.rateLimit.requestsPerMinute |
integer | 5000 |
5000 |
restart | Requests per minute for one credential. Zero is unlimited, which is the right default for an instance that has not thought about it: a ceiling nobody chose that starts refusing agent calls is a worse first day than no ceiling. |
api.apiKeys.rateLimit.anomalyMultiplier |
integer | 10 |
10 |
restart | Alert when a credential exceeds this multiple of its OWN rolling baseline. A key that suddenly enumerates the whole estate looks nothing like itself; a global threshold has to be set for the busiest key and therefore never fires for the others. Zero disables it. |
api.apiKeys.rateLimit.window |
duration | 1m0s |
1m0s |
restart | Accounting window, and the anomaly detector's sampling interval. Defaults to 1m so requestsPerMinute means what it says. |
api.prettyJson |
boolean | true |
true |
reload | Indent JSON response bodies so a person running curl can read them. Defaults to true. HTTP responses only: application logs and audit records stay one object per line whatever this is set to. Turn it off for a bandwidth-sensitive integration; MCP tool results are unaffected either way, because the tool renderer formats the decoded object itself. |
mcp
The MCP listener.
| Field | Type | Default | Example | Reload | Description |
|---|---|---|---|---|---|
mcp.auth.mode |
string, one of oauth | apikey | both |
both |
both |
restart | oauth | apikey | both. This vocabulary is deliberately disjoint from auth.mode's. |
mcp.auth.tokenTtl |
duration | 8h0m0s |
8h0m0s |
restart | How long an OAuth access token minted for an MCP client is valid. It is how long a revocation this instance cannot see takes to bite: a refresh never passes the sign-in proxy, so it is checked against the identity path instead - in local mode the account, which a disable ends at once; in proxy mode the last time the proxy vouched for the person in a browser, and a refresh is refused once that is more than seven days old. Refresh tokens survive a restart; access tokens do not, and a client refreshes silently. Defaults to 8h. |
mcp.oauth.publicUrl |
string | — | "…" |
restart | Base URL an MCP client reaches this instance's API and MCP listener on, e.g. https://searchcode.example.com. Becomes the OAuth issuer and the protected-resource identifier, and a presented token is checked against it. Defaults to the address the request arrived on. |
mcp.oauth.uiUrl |
string | — | "…" |
restart | Base URL a BROWSER reaches the UI listener on. The authorization endpoint is published under it, because that is where the user's session lives. Defaults to publicUrl, then to the address the request arrived on. |
mcp.maxBytesPerCall |
integer | 100000 |
100000 |
restart | Hard ceiling on the bytes one tool call may return. This is the control that does the protecting; truncation is always stated in the text the model reads. Defaults to 100000. |
mcp.serverName |
string | searchcode-private |
searchcode-private |
restart | The key the /account page renders its ready-to-paste MCP client configuration under, and the name used in the claude mcp add line. It is a name local to the reader's client file and must be unique within it; pasting over an existing entry replaces it silently. Letters, digits, dot, dash and underscore, up to 64 characters. Defaults to searchcode-private, which deliberately does not collide with the name searchcode.com publishes for the hosted product. Requires a restart to change. |
mcp.maxResultsPerCall |
integer | 25 |
25 |
restart | DEFAULT number of results a tool returns, not a cap: a caller asking for more gets more, up to the API's hard maximum of 1000 rows a call. A low limit here would make this tool worse than cs at the discovery task the product exists for; the maximum exists because max_results with nothing behind it answered one request with 87.9 MB. A request above it is clamped rather than refused, and a clamp that actually withheld rows says so and names the offset that reaches the rest. Defaults to 25. |
analytics
Who-is-doing-the-work reporting. The default names nobody; attributeAuthors is the operator's own privacy decision and is off.
| Field | Type | Default | Example | Reload | Description |
|---|---|---|---|---|---|
analytics.concentrationFloor |
integer | — | 3 |
reload | Distinct contributors a file needs before the exact count and top share are reported instead of the band "few". 0 uses the default of 3. The right value depends on your team size: in a team of four, "one contributor" identifies somebody whether or not a name is printed. |
analytics.attributeAuthors |
boolean | — | false |
reload | Report WHICH PEOPLE did the work, not just how concentrated it was. OFF by default. Turning it on makes per_user_breakdown read true on /api/v1/usage, because that is exactly what it is. Works-council rules in Germany and France prohibit developer monitoring, so this is the customer's decision and never a default. |
analytics.attributeUsage |
boolean | — | false |
reload | Show a per-person usage table on the admin page: names and record counts over the last seven days, read from this instance's own audit records. OFF by default. NAMES AND COUNTS ONLY, and never on /api/v1/* or MCP. Needs audit.output: file, since on stdout or syslog there is nothing to read back. A SEPARATE decision from attributeAuthors, which is about who wrote the code rather than who uses this. |
advisories
The offline security advisory data. Shipped inside the binary, side-loaded from a signed bundle, NEVER fetched.
| Field | Type | Default | Example | Reload | Description |
|---|---|---|---|---|---|
advisories.bundle |
string | — | "…" |
restart | Path to a signed advisory bundle, side-loaded for an air-gapped site. Empty uses the database compiled into the binary. Requires a restart to change. |
advisories.warnIfOlderThanDays |
integer | 30 |
30 |
reload | Warn on /api/v1/status, in the admin UI, in metrics and in every vulnerability response once a SIDE-LOADED bundle is older than this. 0 turns that sentence off. It does NOT change advisory_database.stale, which is fixed: a database is stale when it is absent, when it is the compiled-in seed at any age, or when a side-loaded bundle is more than 30 days old. The compiled-in seed always says so, because its problem is that it is a seed rather than that it is old. Reloadable. |
serviceAccounts
Non-human principals. The only place a machine credential is declared.
| Field | Type | Default | Example | Reload | Description |
|---|---|---|---|---|---|
serviceAccounts |
list of object | — | […] |
— | Non-human principals. The only place a machine credential is declared. |
serviceAccounts[].name |
string | — | platform-dashboard |
reload | Shown in audit records as the acting principal. |
serviceAccounts[].groups |
list of string | — | [platform] |
reload | REPOSITORY groups this account may reach, named directly — NOT identity groups, and not matched against authz.bindings. Every other groups: in this file is an identity group, which is why this one says so. Read LIVE from this file on every request, unlike a human key's frozen groups, so revoking access here is immediate. |
serviceAccounts[].role |
string | — | viewer |
reload | A built-in or custom role. |
serviceAccounts[].tokenFile |
string | — | /run/secrets/searchcode-dashboard-token |
reload | File holding the bearer token. A reference, never a value. |
serviceAccounts[].tokenEnv |
string | — | "…" |
reload | Environment variable holding the bearer token. |
branding
White-labelling. Unconditional, on every tier.
| Field | Type | Default | Example | Reload | Description |
|---|---|---|---|---|---|
branding.productName |
string | — | Acme Code Search |
reload | Replaces "searchcode" in the UI chrome and the page title. |
branding.logoLight |
string | — | brand/logo-light.png |
reload | Path to the logo shown on a light background. Relative to this file's directory, and confined to it: a path outside, or inside instance.dataDir or /run/secrets, is refused. The file is read ONCE at load and served from memory, and it must be a raster image — the type is sniffed, never taken from the extension, and SVG is refused because it carries scripts. |
branding.logoDark |
string | — | brand/logo-dark.png |
reload | Path to the logo shown on a dark background. Same boundary and same accepted types as logoLight. |
branding.favicon |
string | — | brand/favicon.ico |
reload | Path to the favicon. Same boundary and same accepted types as logoLight. |
branding.primaryColour |
string | — | #2563eb |
reload | A hex colour: #rgb, #rgba, #rrggbb or #rrggbbaa. Hex only, because it is interpolated into a stylesheet — this is a CSS injection surface, and a colour NAME cannot be told apart from a misspelling. |
branding.hideSearchcodeBranding |
boolean | false |
false |
reload | Remove the "powered by searchcode" footer. |
branding.supportUrl |
string | — | https://acme.example.com/support |
reload | Where an error page tells the user to go for help. |
branding.footerHtml |
string | — | "…" |
reload | Sanitised before rendering. This is a stored-XSS surface. |
observability
Metrics and tracing.
| Field | Type | Default | Example | Reload | Description |
|---|---|---|---|---|---|
observability.metrics.enabled |
boolean | true |
true |
restart | Serve the Prometheus endpoint. Defaults to true; the listener is separate, so turning it off is a decision about scraping rather than about exposure. |
observability.metrics.listen |
string | 0.0.0.0:9090 |
0.0.0.0:9090 |
restart | Address to bind for /metrics, host:port. Separate from server.listen so a NetworkPolicy can expose it to Prometheus without exposing it publicly — repository names appear in labels. Requires a restart to change. |
observability.tracing.enabled |
boolean | false |
false |
restart | Off by default. One of the three things that can make this process open an outbound connection — the others are a connections.sources entry and a remote syslog audit sink — and it points at your own collector, never at a vendor. |
observability.tracing.endpoint |
string | — | "…" |
restart | The customer's own OTLP collector. Never a vendor endpoint — searchcode never phones home. |
observability.tracing.sampleRate |
number | 1 |
1 |
reload | Fraction of requests traced, 0.0 to 1.0. Defaults to 1.0, which only matters once enabled is true. |
observability.pprof.enabled |
boolean | false |
false |
restart | Serve Go's profiling handlers under /debug/pprof on the METRICS listener. Off by default. ESCALATION: anything that can scrape metrics can then dump the heap, and a heap dump of this process contains customer source code — so turning this on makes metrics access equivalent to source-code access. Turn it on for as long as it takes to collect a profile and turn it off again. Requires a restart to change. |