Backends grow from "flag + env scrub" to four knobs — render, mcpArgs,
mcpEnv, forbidEnv — so a CLI can receive sherlock's MCP servers however
it accepts them, without ever editing the operator's own config:
- codex: repeated `-c mcp_servers.<name>....` TOML overrides, which
layer over ~/.codex/config.toml. $CODEX_HOME is avoided on
purpose: it also holds auth.json.
- opencode: $OPENCODE_CONFIG pointing at a rendered file in opencode's
own schema; it is one layer of that CLI's config merge.
- goose: repeated `--with-extension <name>:<env> <command line>`.
Those belong to a subcommand, so MCP flags are appended
after the operator's arguments and a bare `sherlock goose`
defaults to `goose session`.
Verified against the real CLIs (codex 0.153.2, opencode 1.18.27, goose
1.49.0): codex registers both servers with args and env intact, opencode
and goose actually spawn them, and SHERLOCK_LOCAL_CONFIG reaches the MCP
process in every case.
Gemini and Cursor CLIs are deliberately left out: they read MCP servers
only from config files they own, so wrapping them would mean writing
into ~/.gemini, ~/.cursor or the checked-out repository.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: fdf86527-ca08-420d-89ee-47bcee60bebe
4.2 KiB
Agents
Sherlock dispatches built-in agent profiles from internal/agent/.
| Agent | CLI | How MCPs are injected |
|---|---|---|
copilot |
GitHub Copilot CLI | --additional-mcp-config @<path> |
claude |
Claude Code CLI | --mcp-config <path> |
codex |
OpenAI Codex CLI | repeated -c mcp_servers.<name>.... |
opencode |
opencode CLI | $OPENCODE_CONFIG=<path> |
goose |
Block Goose CLI | repeated --with-extension <name>:<cmd> |
sherlock <agent> [args...] and sherlock run <agent> [args...] are
equivalent. Unknown agent names exit with usage errors.
Every mechanism above is per-invocation and additive: sherlock never
edits an operator's persistent CLI config, and the CLI's own servers
keep working alongside sherlock's. That rules out CLIs which only read
MCP servers from a config file they own (Gemini CLI, Cursor CLI), since
supporting those would mean writing into ~/.gemini, ~/.cursor or the
checked-out repository.
Spawn behavior
On spawn, sherlock resolves installed MCP binaries (gitea-mcp, grafana-mcp,
gssh-mcp, searxng-mcp), skips missing and disabled ones, hands the rest to
the agent by whichever mechanism that CLI supports, and execs it. For the
file-based CLIs the file is written 0600 under $XDG_RUNTIME_DIR/sherlock/
(<agent>.mcp.json, or <agent>.opencode.json for opencode). Each MCP entry
carries SHERLOCK_LOCAL_CONFIG when a project-local config is in effect, so the
MCP resolves the same config sherlock did — goose takes that inline in the
--with-extension value, which is [name:]ENV=val command args....
The child mostly inherits the parent environment. The Claude profile strips
ANTHROPIC_API_KEY so a personal key does not override sherlock-managed
behavior.
Argument order follows each CLI: codex's -c overrides are global, so they
precede the operator's arguments, while goose's --with-extension belongs to a
subcommand and therefore follows them (a bare sherlock goose defaults to
goose session).
Changing agents
Adding a built-in agent is a code change under internal/agent/. Implement
the small agent interface, register it in init, and use internal/mcp
for config rendering. The exact API lives in internal/agent/agent.go;
shared spawn helpers live in internal/agent/exec.go, and the per-CLI
MCP/-env conventions ("backends") live in internal/agent/backend.go.
A backend has four knobs: render (write the CLI's config file, if it reads
one), mcpArgs (flags, whether pointing at that file or carrying the servers
inline), mcpEnv (env vars, such as a config path a CLI only takes that way),
and forbidEnv (env to scrub).
Custom agents (config)
Operators can register their own agents from config.toml without
touching code — handy for wrapper commands such as a copilot-local
script that points the Copilot CLI at a self-hosted model:
[agents.copilot-local]
command = "copilot-local" # binary to exec; defaults to the name if omitted
backend = "copilot" # which CLI's MCP/-env conventions to use
backend must be one of the built-in backends (claude, codex, copilot,
goose, opencode); it tells sherlock how to hand that CLI its MCP servers and
what env to scrub. Any rendered file is named after the agent
(<name>.mcp.json).
sherlock copilot-local [args...] then dispatches exactly like a
built-in. A custom agent whose name collides with a built-in agent, or
that names an unknown backend, is a hard startup error. (Names matching a
reserved subcommand — status, logout, run, update, version —
are shadowed by that subcommand; reach them via sherlock run <name>.)
Agent config is read from the global config plus any project-local
.sherlock.toml (see configuration), so a repository
can add its own agents. An empty section removes an agent from dispatch,
including a built-in one:
[agents.claude] # not offered in this project
Disabled services
Services switched off with an empty [services.<name>] section are left
out of the rendered MCP config, so the agent never sees that MCP.