Configuration
One file, searchcode.yaml, plus optional conf.d/ fragments. There is no
database of settings, no admin UI that writes config, and no environment
variable that overrides a field — configuration is a file you can commit,
review and diff.
| Configuration reference | Every field, with type, default and an example. Generated from the source. |
| schema.json | The JSON Schema. Point your editor at it. |
searchcode config example prints the annotated example — every block that
exists, with the reasoning beside it. It is the file to start from, and the
same file is on Example configuration.
A minimal configuration
This is enough to start an instance with local accounts, two repositories and one rule about who may read them:
instance:
dataDir: /var/lib/searchcode
auth:
mode: local
repositoryGroups:
- name: platform
- name: payments
repositories:
- url: https://github.com/acme/platform-api
groups: [platform]
- url: https://github.com/acme/billing
groups: [payments]
authz:
adminGroups: [platform-admins]
bindings:
- name: "Engineers read the platform"
groups: [engineering]
repoGroups: [platform]
role: viewer
Every repository names at least one group, and access is granted over groups,
never over repositories. Here engineering reads platform-api, and nobody can
read billing, because no binding names payments and anything not granted is
denied. A private repository also needs a credentials entry, which names a
file or environment variable holding the secret; see Configuration reference.
Four rules that apply to the whole file
No secret is ever inline. There is no field to put one in. A credential is
named by a *File or *Env reference and resolved at load into a store that
redacts itself under every print verb. That is what makes this file safe to
commit to git, safe to render into a ConfigMap 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 authz block is worse than one that refuses
it — the operator believes a control is in force that is not.
Validate before you deploy. searchcode config validate prints every
problem rather than the first, names the field of each, and exits non-zero. Put
it in CI:
searchcode config validate -f /etc/searchcode/searchcode.yaml
Fail closed. authz.defaultAccess defaults to deny, auth.mode defaults
to local rather than open, and an empty authorisation binding set grants
nothing. A forgotten assignment denies.
conf.d
Files matched by include are merged over searchcode.yaml. It defaults to
conf.d/*.yaml when that directory exists, and paths are relative to the
configuration file's own directory.
/etc/searchcode/
searchcode.yaml
conf.d/
repositories.yaml <- searchcode discover writes this
bindings.yaml
Merge semantics: lists append, maps merge, later scalars win, in filename order.
That ordering is safe for authorisation specifically because authorisation is
allow-only. A principal's access is the union of every binding that matches, with
no deny rule, no exceptGroups, no priority and no override — so evaluation is
order-independent. If it were not, access would depend on filename sort
order, which is not a property anybody should have to reason about.
Reload and restart
The Reload column in Configuration reference is generated from the two tables the product actually enforces:
reload— a change takes effect without a restart. The repository list, repository groups, authorisation bindings, the log level, the search budget, the trace sample rate and branding.restart— a reload carrying a change here is rejected, naming the field, rather than applied in part or silently ignored. Listen addresses,instance.dataDir,auth.mode, the metrics listener, and whether tracing is enabled and where it points.—— in neither documented list. A change is picked up only where the component re-reads it, so restart to be sure.
Silently ignoring a change is how somebody spends a day convinced authorisation
is broken when their edit simply never took. Applying it in part is worse: an
instance whose dataDir has moved under it is serving from one directory and
writing to another.
The file is polled once a second. Reload failures are counted and published on
/api/v1/status, because a reload that failed is invisible in the logs of a process
that is still serving correctly.
Two settings that get "corrected"
server.writeTimeout is deliberately 0. A blanket write deadline
truncates streaming MCP responses and cuts off slow searches over large estates,
and both fail as a truncated response rather than as an error — the worst way
for a timeout to present. Per-request deadlines are applied in the handlers,
where the right budget is known.
Operation timeouts are not in server:. server: is transport only. Search
has search.timeout, analysis has analysis.perFileTimeout, ingestion has its
own. One timeout knob meaning seven things gets tuned for the wrong reason.
Your editor
The first line of the annotated example is:
# yaml-language-server: $schema=https://searchcode.com/schema/searchcode.schema.json
Your editor fetches that; nothing in this process ever does. On an air-gapped machine, point it at the local copy instead:
# yaml-language-server: $schema=./schema.json
searchcode config schema prints the same document for the build you are
running.