3.8 KiB
Agents
How sherlock decides which CLI to spawn when you type
sherlock copilot or sherlock claude, and how to add a new one.
Supported today
| Name | Binary | MCP config flag | Notes |
|---|---|---|---|
copilot |
copilot (npm @github/copilot) |
--additional-mcp-config @<path> |
Augments user's ~/.copilot/mcp-config.json. JSON shape is the canonical .mcp.json schema ({"mcpServers": ...}). |
claude |
claude (npm @anthropic-ai/claude-code) |
--mcp-config <path> |
Same {"mcpServers": ...} shape. ANTHROPIC_API_KEY is stripped from the child env so a personal key can't override the sherlock-managed session. |
Routing
sherlock copilot [args...] ⇢ runs copilot
sherlock claude [args...] ⇢ runs claude
sherlock run copilot [args...] ⇢ same, explicit form
sherlock run <unknown> ... ⇢ exit 2 with "unknown agent"
sherlock <unknown> ⇢ exit 2 with "unknown subcommand"
The run form exists for parity with cargo run / npm run; the
bare alias is the daily-driver form.
What sherlock does per spawn
keyring.Open()— fail fast if the OS keyring isn't available (returns*UnavailableErrorwith a remediationHintfield).- Resolve the agent binary on
$PATH. Friendly error if missing. - Render the per-agent MCP config to
$XDG_RUNTIME_DIR/sherlock/<agent>.mcp.json(0600). In Phase 1 the servers map is always empty; Phase 2 populates it fromservices.d/. - Build the child argv with the agent-specific flag.
- Build the child env: parent env minus per-agent forbids. MCPs spawned by the agent will reach into the OS keyring (via
internal/authn.Ensure) on their own at startup — sherlock does not pre-authenticate anything. syscall.Exec— sherlock disappears, the agent takes its place.
Adding a new agent
It's a code change, deliberately. The TOML-overlay design was tried and scrapped: each CLI has enough idiosyncrasies (auth subcommands, permission flags, MCP config schema, env var quirks) that a Go file per agent is honest about the surface area and gives those quirks a real place to live.
Drop a new file in internal/agent/:
// internal/agent/aider.go
package agent
import "gitea.alexandru.macocian.me/Charlie/sherlock/internal/mcp"
func init() { Register(&aider{}) }
type aider struct{}
func (aider) Name() string { return "aider" }
func (aider) Description() string { return "Aider AI pair programmer" }
func (a aider) Spawn(ctx Context, args []string) error {
bin, err := LookPath("aider")
if err != nil {
return err
}
// Aider's MCP schema and flag would go here.
_ = bin
_ = ctx
_ = args
return nil
}
That's the whole API: Name, Description, Spawn. The CLI picks
it up automatically through the init() registry call; sherlock status shows it; sherlock aider ... dispatches.
Reusable helpers
Available to every agent implementation in this package:
| Helper | Purpose |
|---|---|
LookPath(name) |
exec.LookPath with a sherlock-friendly error message. |
BuildEnv(forbid, set) |
parent env minus forbid, plus set. |
DefaultExecer |
the package-level Execer (swap in tests). |
mcp.Render(name, servers) |
writes {"mcpServers": ...} to $XDG_RUNTIME_DIR/sherlock/<name>.mcp.json. |
If a new agent needs a third MCP-config schema, add a new Render*
function to internal/mcp/ rather than open-coding JSON in the agent
file.
What sherlock does not do
- Read agent config from
~/.config/sherlock/agents.d/— that directory does not exist. - Hot-reload registered agents — the registry is sealed at process start, by design (one fewer code path).
- Sandbox the agent — sherlock just
execs it, the agent inherits the user's full environment minus a few targeted forbids.