Files
sherlock/docs/agents.md
T
amacocian 1c2108dd2b
Release / tag (push) Successful in 2s
CI / build (push) Successful in 3m5s
Fix fmt
2026-06-13 13:40:24 +02:00

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

  1. keyring.Open() — fail fast if the OS keyring isn't available (returns *UnavailableError with a remediation Hint field).
  2. Resolve the agent binary on $PATH. Friendly error if missing.
  3. 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 from services.d/.
  4. Build the child argv with the agent-specific flag.
  5. 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.
  6. 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/amacocian/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.