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
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:
- Project-local:
.sherlock.toml, searched for in the working directory and its parents (first match wins).$SHERLOCK_LOCAL_CONFIGpins an explicit path; setting it to an empty value disables the search. - 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 optionalclient_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 requiresusernameandpassword_env(the env var holding the account's app password); the OAuth identity (issuer/client_id, or aprovider) is used as usual. No browser is opened and no refresh token is stored.token_envnames an env var holding a static bearer/PAT. When set, the service bypasses OAuth entirely and sends that token verbatim (gitea-mcp uses thetoken <PAT>scheme). A static token needs nogrant,issuer, orclient_id; combiningtoken_envwithgrantis 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.