Files
sherlock/docs/configuration.md
T
amacocianandCopilot 87e6a0cc49
Charlie/project-charlie: Build image / config (push) Successful in 1s
Charlie/project-charlie: Build image / build (push) Skipped
Charlie/project-charlie: Deploy runners / config (push) Successful in 1s
Charlie/project-charlie: Deploy runners / deploy-morgott (push) Skipped
Charlie/project-charlie: Deploy runners / deploy-melina (push) Skipped
Charlie/project-charlie: Deploy stack / config (push) Successful in 0s
Charlie/project-charlie: Deploy stack / deploy (push) Skipped
Release / release (push) Successful in 1m48s
config: layer a project-local .sherlock.toml over the global config
Sherlock now reads an optional .sherlock.toml found by walking up from
the working directory and layers it on top of the global config. Each
[oauth]/[providers.x]/[services.x]/[agents.x] entry the local file
declares replaces the same-named global entry wholesale; everything else
falls back to the global file. Either file may be absent.

An empty section means disabled: a disabled service is left out of the
rendered MCP config (and its MCP refuses to start if launched directly),
and a disabled agent is removed from dispatch, built-ins included.

The resolved local file is pinned on spawned MCPs via
SHERLOCK_LOCAL_CONFIG so a child with a different working directory
still reads the same config.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: fdf86527-ca08-420d-89ee-47bcee60bebe
2026-09-04 09:14:35 +02:00

5.0 KiB

Configuration

Sherlock reads deployment values from TOML: a global file, plus an optional project-local .sherlock.toml layered on top. Missing sections or required fields are hard errors.

Default source: config.example.toml.

Setup and update compare an existing global config against the shipped example. Missing provider/service sections are appended as marked templates; existing sections and values are not rewritten.

Location

Sherlock reads up to two files and layers them:

  1. Project-local: .sherlock.toml, searched for in the working directory and its parents (first match wins). $SHERLOCK_LOCAL_CONFIG pins an explicit path; setting it to an empty value disables the search.
  2. Global: $SHERLOCK_CONFIG, else $XDG_CONFIG_HOME/sherlock/config.toml, else $HOME/.config/sherlock/config.toml.

The local file wins. Each [oauth], [providers.<name>], [services.<name>], and [agents.<name>] entry it declares replaces the global entry of the same name wholesale — there is no field-level merging, so a local service must carry either its own provider reference or a full inline identity. Entries the local file does not mention come from the global config. Either file may be absent; having neither is an error.

Sherlock passes the local file it resolved to the MCPs it spawns (SHERLOCK_LOCAL_CONFIG), so they read the same config even if their working directory differs.

Disabling

A section with no keys means disabled:

[services.gssh]   # local file: this project does not use gssh
[agents.claude]   # ...and does not offer the claude agent

A disabled service is not offered to agents (its MCP is not spawned), and the MCP refuses to start with a "disabled" error if launched directly. A disabled agent is removed from dispatch, including a built-in one. This is what makes a local file subtractive as well as additive. A section that is absent entirely is not disabled — it is simply missing, which remains an error when something needs it.

Example: a project-local file

# ./.sherlock.toml — this repo talks to a different Grafana, has no use
# for gssh, and adds a wrapper agent.
[services.grafana]
provider = "sherlock-cli"        # provider still resolves from the global file
base_url = "https://grafana.internal"

[services.gssh]

[agents.claude]

[agents.copilot-local]
command = "copilot-local"
backend = "copilot"

Setup and sherlock update only manage the global file; local files are hand-written and never seeded or rewritten. Keep secrets out of one that lives in a tracked repository — reference a global [providers.<name>] instead of copying a client_secret in.

Shape

[oauth] is optional. Set browser = "<executable>" to open OAuth login URLs with a specific browser, for example browser = "firefox". Leave it unset to use the OS default browser.

[providers.<name>] defines a reusable OAuth/OIDC identity: issuer, client_id, optional client_secret.

[services.<name>] defines one MCP target. The service name is also the wallet key used by sherlock status and sherlock logout <name>. Each service requires base_url plus either:

  • provider = "<name>", resolved from [providers.<name>], or
  • inline issuer, client_id, and optional client_secret.

Mixing provider with inline identity fields is rejected.

Headless auth fields

For unattended deployments a service may add one of two headless modes. Secrets never live in the config file: the fields below name environment variables, and sherlock reads the values at startup.

  • grant = "client_credentials" selects the machine grant. It also requires username and password_env (the env var holding the account's app password); the OAuth identity (issuer/client_id, or a provider) is used as usual. No browser is opened and no refresh token is stored.
  • token_env names an env var holding a static bearer/PAT. When set, the service bypasses OAuth entirely and sends that token verbatim (gitea-mcp uses the token <PAT> scheme). A static token needs no grant, issuer, or client_id; combining token_env with grant is rejected.

Empty grant (the default) is the interactive authorization-code + PKCE browser flow. In containers, also set SHERLOCK_KEYRING=memory to avoid the OS keyring (see auth-model.md), and set SHERLOCK_LOCAL_CONFIG to an empty value so a .sherlock.toml in a mounted repository cannot change the service configuration underneath the deployment.

Example:

[services.grafana]
provider     = "sherlock-cli"
base_url     = "https://grafana.example"
grant        = "client_credentials"
username     = "sys-jarvis"
password_env = "JARVIS_GRAFANA_PASSWORD"

[services.gitea]
base_url  = "https://gitea.example"
token_env = "JARVIS_GITEA_TOKEN"

Not configured here

Tokens live in the OS keyring (or an in-memory store in headless mode). OAuth scopes live in MCP code. Agent profiles and the built-in MCP registry live in Go code.

To add a service, add its config entry and wire or install the matching MCP binary.