User guide
This page is generated from
README.mdin the repository.
Website and docs: https://madhu-sv.github.io/garuda/
Garuda is a terminal coding agent. This is version 0.16.1: garuda setup (choose a model and store
its key once) and the sign-in from editors, on top of 0.15.0 (Garuda in your editor over the Agent
Client Protocol, garuda acp) and 0.14.1 (the 0.14–0.17 branches after the merge gate and Garuda’s own
review; see the notes under the status table). Release notes: CHANGELOG.md.
Design documents: docs/ (architecture, high-level design, low-level design per component).
The requirements doc defines the scope. Code, tests and commits refer to its IDs (F1–F26, N1–N8).
Status
Section titled “Status”| Milestone | State |
|---|---|
| M1 Skeleton and fake model | Done |
| M2 Read-only tools | Done |
| M3 Write tools and permissions | Done |
| M4 Limits, context, sessions | Done |
| M5 CLI polish and evals | Done |
| 0.2: OS sandbox, Ink chat, MCP client (stdio), web_fetch, hooks | Done |
| 0.3: open models (OpenAI-compatible), Java and Python profiles and evals, explore subagent (opt-in), stream retries | Done |
| 0.4: AGENTS.md/CLAUDE.md, custom slash commands, plan mode, todo tool (opt-in), LSP diagnostics (opt-in), undo, remote MCP with OAuth | Done |
0.5: garuda init (AGENTS.md, migrate from other agents), JSON output (Claude Code format), skills, custom agents, web search |
Done |
| 0.6: chat UX (Esc, multi-line, $EDITOR; @file, !cmd, completion; sessions and models in the chat; focus-aware notifications, /diff), Claude’s web search | Done |
| 0.7: scheduled jobs (plan now, build later with an approval list, own branch), launchd, Batch API (step limit, finish-by time) | Done |
0.8: chat UX from the second OpenCode comparison (/compact, argument completion, fuzzy @, session rename and delete, /plan <task>) |
Done |
0.9: /details, /thinking (effort, show), Claude’s thinking blocks kept, command palette (Ctrl-P) |
Done |
| 0.10: formatters after edits (opt-in; measured: +14% cost, no gain) | Done |
| 0.11: night shift (a queue of overnight jobs, one digest, a launchd agent) and proof of work (tests before and after, risk flags, a principal-engineer review) | Done |
| 0.12: benchmark your repo (eval tasks from the repo’s own git history), output-limit recovery | Done |
| 0.13: network allowlist for commands (W6): presets or hosts, through Garuda’s proxy, off by default | Done |
| 0.14: sandbox launch probe, core modularization, background daemons (opt-in), interactive patch staging (W4) | Done (0.14.0) |
| 0.15: multi-language code intelligence (Python, Java, TS/JS, caller candidates, impact, /callers, /defs, /impact, ast_query) | Done (0.14.0) |
| 0.16: language plugins (Go and Rust built in, user plugins in ~/.garuda/languages, /languages; project plugins not loaded) | Done (0.14.0) |
| 0.17: Mixture-of-Experts subagents (opt-in), team policy and audit log (W5) | Done (0.14.0) |
Editors over the Agent Client Protocol (garuda acp: VS Code with an extension, Zed, JetBrains, Neovim, Emacs) |
Done (0.15.0) |
garuda setup (model and stored key) and the editor sign-in (ACP Terminal Auth) |
Done (0.16.0) |
The rows labelled 0.14–0.17 are branch labels, not versions: the “0.15” and “0.16” branches (code intelligence, language plugins) shipped in 0.14.0. The last two rows are versions 0.15.0 and 0.16.0.
The 0.14–0.17 rows were built on stacked branches that were never released. They ship together as 0.14.0 after the merge gate: the high findings of the branch review are fixed (policy source, audit log, hunk approval U0, daemons, MoE, project plugins), and features with no measured gain are off by default (MoE, daemons). G02 to G08 are fixed too. The open gap G09 is listed in docs/quality-baseline/issue-register.md. The section headings below keep the branch labels (0.14–0.17).
Before 0.14.0, Garuda reviewed its own code with Fable 5.1, one area at a time (knowledge, permissions, tools, sandbox, audit, loop, agents), in plan mode. 0.14.1 adds the cli and extensions areas and fixes the open findings of all nine areas (see CHANGELOG.md). Known limits in 0.16.1:
- G09: project language plugins are not loaded (no consent flow for their imports yet).
- Token budget: a call’s reservation is an estimate (about 4 characters per token, plus the output limit), so a provider that counts more input tokens than the estimate can pass the cap by that difference.
- Audit log: without the checkpoint (
"audit": { "checkpoint": true }, off by default), lines cut from the end of a file, or a removed file, are not detected. - On macOS, secret-named variables other than the provider API keys stay in Garuda’s own environment.
- Build tool caches that the sandbox may write (
~/.gradle/wrapper,jdks,native,caches,~/.m2/wrapper) hold programs that a build runs later outside the sandbox. They stay writable, because./gradlewneeds them in the sandbox; a sandboxed command could change them. - The jdtls download is checked against the
.sha256file of the same server, not a pinned hash. - Editors (
garuda acp): Garuda’s session history is not in the editor (nosession/load), the editor’s MCP servers are not started, Garuda reads saved files (not unsaved buffers), the terminal’s own commands (/undo,/diff…) are not available, and an edit is approved as a whole (no hunks). - Stored keys are in a file (
~/.garuda/credentials, mode 0600, hidden from the sandbox), not in the OS keychain yet. Keep the file out of backups and dotfile sync. - Native Windows is not supported; use WSL.
Install (0.14.1 and later) with Homebrew or npm. You need Node 22 or later; Homebrew installs it.
npm install -g @garuda-agent/garudaOr with Homebrew (macOS and Linux). Homebrew loads a formula from a third-party tap only after you trust it, so trust the Garuda formula first:
brew tap madhu-sv/garudabrew trust --formula madhu-sv/garuda/garudabrew install garudaOr build from source. You need Node 22 or later, pnpm 10 and git. Node 25 and later do not include corepack, so install pnpm with npm.
npm install -g pnpm@10git clone https://github.com/madhu-sv/garuda.git && cd garudapnpm installpnpm check # typecheck + lint + testspnpm buildpnpm link --global # optional: puts `garuda` on your PATH (run `pnpm setup` once first)Then:
garuda setup # once: pick a provider and model, enter the keygaruda # chat in the current foldergaruda -p "Explain what this repo does" # run one task and exitecho "Explain this repo" | garuda # the same, from stdinFrom a source build without pnpm link, use node dist/cli/index.js in place of garuda.
garuda setup (0.16) asks for the provider, the model and the key (hidden input), checks the key
with one free request to the provider, and stores the model as "default" in
~/.garuda/models.json and the key in ~/.garuda/credentials (mode 0600; commands in the sandbox
cannot read it). garuda setup --show shows the model and where each key comes from (masked);
garuda setup --forget [NAME] removes stored keys. Environment variables still work and win:
ANTHROPIC_API_KEY, GARUDA_MODEL. Do not sync or back up ~/.garuda/credentials to other places.
Tools: read_file, glob and grep run with no question.
write_file and edit_file show a diff first. You pick: allow once, allow for this session, or deny.
Commands (bash) run in an OS sandbox when your machine has one (see Sandbox below). There they run
with no question: the sandbox lets them write only in the project, temp folders and package caches,
with no network. So a command can change project files without a question; /undo restores the
files in its snapshot (ignored files, such as build output, are not in it). A command outside the
sandbox asks, unless one of your allow rules covers it; the strict profile forbids it. Without a
sandbox, each command asks first, unless one of your allow rules covers it, so read each one before
you allow it.
Garuda removes a leading cd <working root> && from a command, because each command already starts there.
When the model reads files with bash, or pipes into head or tail, the result adds a short [Garuda: …] note that tells it to use the file tools.
For scripts and CI, --output-format json prints one JSON result, and --output-format stream-json
prints one JSON object per line as the task runs. The fields are those of Claude Code’s headless mode
(claude -p --output-format …), so the same scripts work:
g -p "Run the tests" --output-format json | jq -r '.subtype, .total_cost_usd, .result'g -p "Fix the lint errors" --output-format stream-json --verbose > run.jsonltotal_cost_usd is always a number. Check total_cost_is_unknown (no price) and
total_cost_is_incomplete (a known minimum, for example after a failed attempt) before you use it
as an exact cost.
stdout holds only JSON; warnings and errors go to stderr (--verbose adds the tool activity). The exit
code does not change. Details: docs/lld/cli.md.
Try it on this repo:
node dist/cli/index.js -p "Where is runAgent defined, and what does it do?"node dist/cli/index.js -p "Run the tests and tell me the result"Models
Section titled “Models”-m (or GARUDA_MODEL) takes a Claude model id, or <provider>/<model> for other providers:
g -m claude-sonnet-5 # Anthropic (ANTHROPIC_API_KEY)g -m ollama/qwen3-coder:30b # Ollama on this machineg -m lmstudio/<model> # LM Studio (also: llamacpp/…, vllm/…)g -m openrouter/qwen/qwen3-coder # OpenRouter (OPENROUTER_API_KEY)g eval -m ollama/qwen3-coder:30b -s hard # measure an open model on the eval tasksSet the context window, price and output limit per model, and add providers, in ~/.garuda/models.json:
{ "providers": { "lab": { "type": "openai-compatible", "baseUrl": "https://llm.example.com/v1", "apiKeyEnv": "LAB_KEY" } }, "models": { "ollama/qwen3-coder:30b": { "contextWindow": 65536, "maxTokens": 8192 } }, "default": "ollama/qwen3-coder:30b"}- Only this file in your home folder can define providers; a project cannot send your code elsewhere.
Keys come from environment variables. Plain http works only to this machine (or with
"allowInsecureHttp": true). "default"is the model when neither--modelnorGARUDA_MODELnames one.- Some small models write a tool call as JSON text. When the whole reply is such a call to a known
tool, Garuda runs it as a real call, with the usual approvals. For a model that also writes
sentences around the call (for example
qwen2.5-coder:7b), set"textToolCalls": "lines"for that model: then a call on its own line runs too. A call in the middle of a sentence never runs. - Without a
contextWindow, Garuda assumes 32 768 tokens for open models and says so. The server must allow the window too: for Ollama, start it withOLLAMA_CONTEXT_LENGTH=65536 ollama serve. - Local models cost $0. Tool calling quality varies by model: measure a model with
g evalfirst.
Chat mode
Section titled “Chat mode”garuda with no task starts a chat. Each line is one task; the conversation carries over.
/help,/usage(tokens and cost),/session(id and file),/new(new session),/commands(your custom commands),/planand/build(plan mode),/undoand/redo,/lsp(language servers),/init(set up this folder, see below),/editor,/compact,/details,/thinking,/diff(see Undo),/scheduleand/jobs(see Scheduled jobs),/exit. Your custom commands and skills run with/name./sessionslists this project’s sessions (newest first: time, first prompt, turns, cost, model)./sessions <number or id>continues one in the chat, as--resumedoes. Read tracking starts again./sessions rename <number or id> <title>gives a session a title for the list./sessions delete <number or id>asks, then deletes the session file and its subagent logs for good; the open session cannot go./modelslists the known Claude models and the models in~/.garuda/models.json, with context window and price./models <number, id or opus|sonnet|haiku|fable>switches the main model for the next turns of this chat. The session records the change; a new chat starts with-morGARUDA_MODELagain. The prompt cache starts again; a smaller window makes Garuda compact the conversation first. Explore keeps the start model./export [file]writes the conversation as Markdown in the working folder (defaultgaruda-<session id>.md): prompts and answers in full, one line per tool call. It comes from the session file, so secrets are redacted. It never overwrites a file and never writes outside the folder./compact [what to keep]summarises the older turns now, to free context before a new part of the work. The last 4 steps stay in full. The optional text tells the summary what to keep, for example/compact keep the API decisions. Esc stops it; the conversation then stays as it was. Garuda still compacts on its own when the context gets full./detailshides the result line under each tool call (⎿ 12 line(s)), so a turn shows one line per call. A failed call still shows its result, and Ctrl-O still shows the last output in full./detailsagain, or/details on, shows them. It applies to new lines only./thinkingcontrols Claude’s thinking for this chat. Claude Opus 5.5, Opus 5 and Sonnet 5 always think; Opus 4.6 to 4.8 and Sonnet 4.6 think only after/thinking on./thinking low|medium|high|xhigh|maxsets the effort (less or more thinking;defaultgoes back). Garuda always asks for short summaries of the thinking, so data keeps coming while the model thinks (with no data for 5 minutes, the connection closed and a long answer was lost; Garuda now also waits up to 20 minutes for the next data). By default they are hidden./thinking showshows them: the chat shows their first lines dimmed, and Ctrl-O shows all of it until the next tool output replaces it;hidegoes back. The session records the choice;"thinking": { "effort": "low", "show": true }in settings sets the start value. A change of effort or on/off starts the prompt cache again; show and hide do not. Other providers do not offer it.- Esc or Ctrl-C during a task stops the task and kills its commands (Esc also drops queued lines). The chat goes on, and the model learns with your next message that you stopped the task, so it does not go on with it unless you ask. A second Ctrl-C during the task exits Garuda at once; Esc never exits.
- New lines: end the line with
\and press Enter, or press Alt+Enter (Option+Enter on a Mac with “Use Option as Meta”). Pasted text keeps its new lines and is never sent by itself. @pathattaches a file or folder:fix the bug in @src/cart.ts. The file’s text goes with your message (numbered, up to 2,000 lines) and counts as read, so the model can edit it at once; a folder gives its list of entries. The same rules asread_file: only files in the folder, no secrets (.env, keys), and the same permission check (deny rules, the team policy’sdenyPaths, a link to a secret; 0.14). Garuda shows what it attached. A word after@that is not a path stays text.!commandruns a command yourself, like the bash tool: in the sandbox with no question, deny rules and hooks apply. You see the output, and it goes to the model with your next message. Esc stops it.- Tab completes
/commands(built-in, your own, skills) and@paths; with several matches it lists them. It also completes arguments:/models <model>,/sessions <id|rename|delete>(with each session’s title),/jobs <id|cancel>,/diff last,/mcp logout <server>and/lsp install <language>. - When no path starts with what you typed after
@, Tab searches all files by their letters in order:@rntmfindssrc/app/runtime.ts. One match completes; otherwise the best 10 are listed. The search skips hidden files,node_modules,dist,build,targetand virtual environments. - Ctrl-P opens the command palette (full chat only): every command with its one-line help, then your
own commands and skills. Type to filter:
thkfinds/thinking; when no name matches, words of the help do (markdownfinds/export). ↑/↓ choose. Enter runs a command that needs no arguments (/usage,/undo,/details…) or puts/namein the input line for one that takes them (/models,/sessions,/thinking…), so Tab can complete the rest. Esc or Ctrl-P closes it. - Ctrl-G (or
/editor) opens the prompt in$VISUALor$EDITOR(defaultvi). The saved text comes back into the input line; press Enter to send it. The plain chat also joins lines that end with\. - At the prompt, Ctrl-C clears the line; Ctrl-C twice (within 2 s) or Ctrl-D exits.
- On a terminal, the chat uses Ink (0.2): model text streams, and each finished paragraph gets basic markdown styles. A spinner shows running tools. The footer shows the model, the sandbox, the context use and the cost.
- Type-ahead: type during a task and press Enter to queue the next task. Keys typed before the chat is ready are kept too.
- Ctrl-O prints the full output of the last tool call. ↑ and ↓ browse earlier inputs.
- Approvals show the full diff or command in the scrollback. Answer with ↑↓ and Enter, or
y (once), a (session), n or Esc (deny). For
edit_file, pressh(orp) to review hunks one by one (0.14): only the hunks you stage are written, and the model is told which ones you rejected. A diff too long to show in full has noh. GARUDA_PLAIN=1turns Ink off. Pipes,-p, the evals and the standalone binary always use plain output.- Notifications: when an approval waits during a task, and when a task that ran 10 s or longer ends, Garuda
tells you. In iTerm2, Ghostty and WezTerm it sends a desktop notification (the OSC 9 escape code); in other
terminals and in tmux it rings the bell.
GARUDA_NOTIFY=off|bell|osc9|auto, or"notifications": { "channel": "off", "afterSeconds": 30 }in.garuda/settings.json, changes it. In iTerm2 the notification needs “Send escape sequence-generated alerts” (Settings › Profiles › Terminal), which is on by default. In iTerm2, Ghostty, WezTerm and the VS Code terminal, the full chat turns on focus reporting: while Garuda’s window has focus, no notification goes out. In other terminals, in tmux and in the plain chat, every notification goes. - Files are written atomically (a temporary file, then a rename), so an exit never leaves half a file.
garuda --resumecontinues the latest session in chat mode.
Model text goes to stdout; tool activity and notes go to stderr. So garuda -p "…" > answer.md keeps only the answer.
Editors (ACP, 0.15; sign-in 0.16)
Section titled “Editors (ACP, 0.15; sign-in 0.16)”garuda acp runs Garuda as an agent for editors that speak the Agent Client Protocol. The editor
starts it, shows the conversation, the tool calls and the questions, and Garuda does the work with
the same permission engine, OS sandbox, team policy, hooks and audit log as in the terminal.
Run garuda setup once in a terminal (see Use): then the editor needs no key in its settings.
VS Code, with the extension “ACP Client” (formulahendry.acp-client), in the user settings
(Cmd+Shift+P, Preferences: Open User Settings (JSON)). Use the full path from which garuda:
VS Code started from the Dock does not get your shell’s PATH.
{ "acp.agents": { "Garuda": { "command": "/opt/homebrew/bin/garuda", "args": ["acp"] } }}Zed: the same command and argument under agent_servers (with "type": "custom"). With no model
or key, Zed offers Set up Garuda (ACP Terminal Auth, 0.16): it runs garuda acp setup in a
terminal, and the next thread works. The website’s
Editors page has the steps and what the editor
shows.
- Each edit asks in the editor, with the editor’s diff view; “Allow for this session” lasts for this session only. Keep an extension’s auto-approve off: it answers Garuda’s questions for you.
- The editor’s stop button ends the turn: a running command stops, and a waiting question is a deny. Each call that the stop ended shows as failed, with a line such as “Garuda: $ sleep 60 cancelled.” A command that exits with an error shows as done (the model reads the error); a refused call shows as failed.
- Build and plan modes are in the editor’s mode menu; custom commands and skills are in its
/menu. Built-in chat commands (/undo,/diff…) are only in the terminal in this version. - Garuda reads and writes files and runs commands itself; it does not use the editor’s file or
terminal access, so it reads the saved file, not an unsaved buffer. It does not start MCP servers
that the editor sends (its own
~/.garuda/mcp.jsonand the project’s work as usual). - Project settings that loosen safety apply only after a yes in a terminal (
garudain that folder).
Design: docs/lld/acp.md.
Init: set up a folder
Section titled “Init: set up a folder”garuda init (or /init in the chat) sets up the current folder:
- It reads the files of other coding agents: Claude Code, OpenCode, Codex, Gemini CLI, Tabnine, Cursor
and Copilot. It shows what goes where (MCP servers, slash commands, permission rules), then asks once.
It writes new files only: an existing
mcp.json,settings.jsonor command file is never changed. It adds Garuda’s local folders to.gitignore. Secret values (tokens, keys) are never copied; they become${NAME}, so set them in your environment. - If the folder is not a git repository, it offers
git init. - In a code project, the model reads the code and writes or improves
AGENTS.md(build and test commands, structure, conventions). It also reads other agents’ instruction files (GEMINI.md,.cursorrules,.github/copilot-instructions.md…) and carries over what applies. In an empty folder, it asks what you want to build first.
When a folder has no AGENTS.md, or has another agent’s files, the chat start shows a hint to type /init.
Imported project servers and commands still ask for consent the first time. Details: docs/lld/init.md.
Custom slash commands
Section titled “Custom slash commands”Save a prompt as a Markdown file and run it with /name:
<!-- ~/.garuda/commands/review.md (yours) or .garuda/commands/review.md (the project's) -->---description: Review a file for bugsargument-hint: <path>---Review $1 for bugs and missing tests. List the problems by severity./review src/cart.tsin the chat, orgaruda -p "/review src/cart.ts".$ARGUMENTStakes all arguments,$1…$9one each./commandslists them;/helpshows them too.- A subfolder gives a name with a colon:
frontend/test.md→/frontend:test. - A command is only a prompt. Its tool calls ask or run in the sandbox as usual.
- A project command shows its full text and asks the first time. “Remember” pins the answer to the file; a changed file asks again. Built-in commands, and your own commands, win over project commands with the same name.
Skills
Section titled “Skills”A skill is a folder with a SKILL.md: instructions for one kind of task, in the
Agent Skills format that Claude Code uses. Garuda reads your
Claude Code skills where they are.
<!-- ~/.garuda/skills/release-notes/SKILL.md (or ~/.claude/skills, .garuda/skills, .claude/skills) -->---name: release-notesdescription: Write release notes from the git log. Use when the user asks for release notes or a changelog.---Read `git log --oneline $0..HEAD`. Group the changes into Added, Changed and Fixed. Followreferences/style.md in this folder.- The model sees each skill’s name and description. When a task matches, it loads the skill with the
skilltool, and reads the skill’s other files (references/,scripts/) only when it needs them. - Run one yourself:
/release-notes v0.4.0.$ARGUMENTS,$0,$1… and${CLAUDE_SKILL_DIR}work as in Claude Code. A skill wins over a custom command with the same name./commandslists them. - A project skill shows its full text and asks the first time it loads. “Remember” pins the answer to SKILL.md; a changed file asks again. Your own skills win over project skills with the same name.
disable-model-invocation: true: only you can run it.user-invocable: false: only the model can.allowed-toolsis ignored: every call still asks or runs in the sandbox.- Nothing changes when you have no skills.
"skills": { "enabled": false }in.garuda/settings.jsonturns them off. Details: docs/lld/skills.md.
Plan mode
Section titled “Plan mode”In plan mode the agent reads the code and writes a plan; it cannot change anything.
/planand/buildswitch the mode; in the chat, Shift+Tab toggles it.garuda --planstarts in plan mode. The footer showsPLAN./plan <task>switches and plans the task at once (/build <task>too).- Allowed: reading tools, and bash in the sandbox, which then cannot write the project (temp folders and
package caches only), so read-only commands and many tests still work. Denied, with no question: file
edits and writes,
remember, commands outside the sandbox, and web_fetch or MCP tools without an allow rule. With no OS sandbox, bash is off in plan mode. - When the plan is ready, Garuda asks “Build this plan?”: build it now, switch to build mode and type the task yourself, or keep planning.
Before each turn, Garuda takes a snapshot of the project. /undo takes back the last turn: its file
changes (edits, new and deleted files, and files that commands changed) and its messages, so the model
forgets it. /redo brings it back.
- It asks first, with the list of files. Changes that you made after the turn go back too.
- Snapshots are in
~/.garuda/snapshots, in a git store of Garuda’s own. Your repository, index and branches do not change, and the project does not need git..gitignorerules apply. - It works after
garuda --resume. After the conversation was compacted, undo restores only the files. - Cost: about 15–90 ms per turn.
"undo": { "enabled": false }in.garuda/settings.jsonturns it off. /diffshows what changed since the first turn of this session: the files with+/−line counts, then the diff (300 lines; Ctrl-O shows all)./diff lastshows only the last turn;/diff <path>or/diff last <path>limits it to one file or folder. It compares with the files now, so your own changes count too. It uses the undo snapshots, so it is off when undo is off.
Formatters (opt-in)
Section titled “Formatters (opt-in)”With "formatters": { "enabled": true } in .garuda/settings.json, Garuda runs the project’s own
formatter on each file that edit_file or write_file changes, and the result shows what it changed:
Edited src/cart.ts.Formatted with prettier:- const total=items.reduce((s,i)=>s+i.price,0)+ const total = items.reduce((s, i) => s + i.price, 0);- It finds Biome or Prettier (their config and
node_modules/.bin), ruff or black (pyproject.toml, a venv or PATH), gofmt (go.mod) and rustfmt (Cargo.toml). It never installs a formatter. "commands"changes the list:{ "prettier": false }turns one off;{ "mine": { "extensions": ["txt"], "command": ["fmt", "$FILE"] } }adds one or replaces a detected one.- Formatters run only in the OS sandbox, like the model’s commands. A failed formatter adds a note; it never undoes the edit.
- Off by default until measured:
garuda eval -s hard --repeat 3 --format on(and--format off).
LSP diagnostics (opt-in)
Section titled “LSP diagnostics (opt-in)”With garuda --lsp, or "lsp": { "enabled": true } in .garuda/settings.json, a language server checks
each file that edit_file or write_file changes, and the result lists its errors:
Edited src/cart.ts.
1 error in src/cart.ts after this change (tsc): 14:7 Type 'string' is not assignable to type 'number'. [2322]- TS/JS: TypeScript 7 (
tsc --lsp),tsgoortypescript-language-server. Python: basedpyright or pyright. Java: jdtls (it needs Java 21 or later;JAVA_HOMEor a usual JDK folder). For a Maven or Gradle project, jdtls starts with the first task, because its project import takes a while. - Garuda uses a server on your PATH (for Java also
brew install jdtls), or its own copy:garuda lsp install typescript(orpython,java) installs a pinned version into~/.garuda/lsp/. With{ "autoInstall": true }in~/.garuda/lsp.json, Garuda asks to install a missing server at the first edit. garuda lsp(or/lspin the chat) shows the servers and their state.- Servers run only in the OS sandbox: the project is read-only for them, and they have no network. A missing, slow or broken server never blocks an edit.
- Off by default: in the A/B eval on the hard suite the model made no type errors, so LSP had nothing to
catch. Measure it again with
garuda eval -s hard --repeat 3 --lsp on.
Todo list (opt-in)
Section titled “Todo list (opt-in)”"todo": { "enabled": true } in .garuda/settings.json gives the model a todo_write tool: for a task
with 3 or more steps it keeps a plan, and the chat shows it as a checklist (✔, ▶, ○). It is off by
default: in the A/B eval on the hard suite the model never called it. Measure it again with
garuda eval -s hard --repeat 3 --todo on.
garuda eval runs eval tasks in scratch folders and reports pass or fail, steps, tokens and cost per task (N5).
A task passes when its check command exits with 0 and the agent did not change the task’s tests.
The 0.1 target is 7 of 10.
garuda eval --list # the tasks of each suitegaruda eval -m claude-sonnet-5 # the basic suite (10 small repos)garuda eval -s hard # the hard suite (6 tasks on a 110-file repo)garuda eval -t fix-add hard-rename --keep # some tasks, keep the scratch foldersgaruda eval -s hard --repeat 3 # each task 3 times, with a mean row per taskgaruda eval -s hard --repeat 3 --index lookup # the same, with find_symbol and find_references (A/B)garuda eval -s java # 5 Maven projects with JUnit 5garuda eval -s python # 5 pytest projectsgaruda eval -s hard --repeat 3 --subagents on # A/B: the same, with the explore subagentgaruda eval --parallel 4 # 4 tasks at the same timegaruda eval --batch on # A/B: model calls through the Batch API (half price, slow)garuda eval -s hard --keep-thinking off # A/B: drop Claude's thinking blocks, as before 0.9garuda eval -s hard --format on # A/B: Biome formats after each edit (and --format off)--batch on (0.7, Anthropic models) sends every model call as a batch of one: half the token price,
but each step waits until its batch ends (usually minutes). It runs all tasks at the same time (up to 20;
--parallel changes it) and gives each task 12 hours. The report shows the share of tokens from the
prompt cache and the wall time, to compare with --batch off.
The Java and Python suites need a toolchain. garuda eval checks it before the first model call and
says what is missing:
- Java: a JDK (17 or later), Maven, and JUnit 5 in
~/.m2. Rungaruda eval --prepare javaonce: it runs Maven on a small project with network, to download the plugins and JUnit. The evals then run offline. - Python:
python3with pytest 7 or later (python3 -m pip install --user pytest).garuda eval --prepare pythonchecks it.
The checks guard against shortcuts: the tests and build files (pom.xml, pyproject.toml) are
protected, the Java check refuses a .mvn folder, and pytest reads only pyproject.toml and no
conftest.py.
Benchmark your repo (0.12)
Section titled “Benchmark your repo (0.12)”Measure Garuda on your own code. --from-git turns recent commits that changed code and tests into
tasks: the agent starts at the commit’s parent, gets the commit message and the commit’s tests, and
passes when the tests pass and it did not change them.
garuda eval --from-git # build .garuda/evals/repo-suite.json from the last 200 commitsgaruda eval --from-git --since 2026-01-01 --max-tasks 10 --test-command "pnpm test"garuda eval -s repo --list # the tasksgaruda eval -s repo -m claude-sonnet-5 # run them (the A/B flags work too)A commit is kept only when its tests fail at the parent and pass at the commit. Garuda runs only the
commit’s test files when it knows the runner; --test-command "make test T={files}" works for others.
Commits that change dependencies, binary files or more than 10 code files are skipped. Building runs your test command twice per
candidate on this machine, with no sandbox, in git worktrees: your checkout does not change.
The runner approves every call except its deny rules (rm -rf, sudo, git push, curl, wget).
Commands, and the check after each task, run in the scratch folders, in the OS sandbox when there is one (--executor auto|os|host). Results and session files go to
.garuda/evals/<run-id>/.
pnpm startup checks that startup takes less than 1 s (N3). The Anthropic SDK and the prompt library
load on first use, not at startup.
Code index and dynamic language plugins (local knowledge)
Section titled “Code index and dynamic language plugins (local knowledge)”Garuda keeps a local code index with a dynamic language plugin architecture (0.16). It answers code questions on this machine, with zero native C++ compilation dependencies and no model call:
- Built-in language plugins: TypeScript/JavaScript, Python, Java, Go, and Rust.
- Dynamic plugin system:
- User plugins: Drop custom language experts in
~/.garuda/languages/<lang>.js. - Project plugins (
<root>/.garuda/languages/) are not loaded (merge gate). A plugin runs inside Garuda’s own process with no sandbox, there is no consent flow yet, and a hash pin of the entry file does not cover the files it imports (G09)./languagesnames the plugins that were skipped. - Tests and evals load the built-in experts only.
- User plugins: Drop custom language experts in
- Tools for the agent (read-only, no approval), chosen with
"codeIndex"in.garuda/settings.json:"off"(default),"lookup"(find_symbol,find_references,find_callers,impact_analysis,ast_query) or"all"(alsorepo_map).find_callersgroups indexed reference sites by their enclosing symbol;impact_analysisestimates dependents, blast radius and affected tests (including Go*_test.goand Rust*_test.rs);ast_queryfilters symbols by kind, container, and wildcards. Java, Python, Go and Rust references use identifier matching, and Garuda cannot vouch for a user plugin. Their references, callers and impact results are marked Approximate, including empty results: matching can include unrelated symbols and miss aliases or dynamic uses. Check with a compiler or language server before a rename. These tools do not run jdtls or pyright. The fixed 60-query measurement records Java precision/recall 61.1%/91.7%, Python 71.4%/93.8%, against frozen language-server reference results. So about 2 in 5 Java hits and 3 in 10 Python hits were not real references. The recall counts only the reference patterns in this generated sample (30 queries per language, one capture, Linux); aliases, dynamic dispatch and real-repository layouts can lower it. This sample does not establish accuracy on your repository. - Chat commands for you:
/where X(or/defs X),/refs X,/callers X,/impact X,/map [folder], and/languages(lists active language plugins, indexed file counts, and standby status). - The code graph (files, exports, imports) is cached in
.garuda/index/code-graph.jsonby file hash. Experts run in-process and preserve single-binary distribution; outside JS/TS, declaration extraction and reference matching use heuristics. - Files follow
.gitignore. Sensitive files are never indexed.
Mixture-of-Experts (MoE) subagent dispatch and /experts (0.17)
Section titled “Mixture-of-Experts (MoE) subagent dispatch and /experts (0.17)”Garuda features an autonomous Mixture-of-Experts (MoE) dispatch architecture for polyglot codebases:
- Specialist profiles: Dedicated subagents for Go, Rust, Python, Java, and TypeScript equipped with language-idiomatic prompts, toolchain testing commands (
cargo test,go test ./...,pytest,mvn test,pnpm test), and the code index tools. - Dynamic delegation: The primary orchestrator agent can invoke
delegate_expert({ language, task, files })to dispatch complex language-specific tasks to child subagents. The child subagent runs with the code index tools (find_symbol,find_references,find_callers,impact_analysis,ast_query,repo_map; only JS/TS uses a language service, the other languages use heuristic matching marked approximate), executes language tests, and returns a synthesized report without cluttering the primary context window. - Chat command:
/expertslists all available language specialists, their active test commands, and their active status / indexed file counts. - Banner & Observability: The startup banner features a dedicated
subagentsrow (subagents: explore · 5 MoE experts (Go, Rust, Python, Java, TS)). In session,/sessionprovides a full breakdown of active subagents, agents, skills, and tools. - Configuration: off by default (no measured gain yet). Turn it on with
"moe": { "enabled": true }in.garuda/settings.json;subagentsalone does not turn it on. Onedelegate_expertcall runs at a time, because a specialist can write files (merge gate, G05).
Team policy and structured audit log (0.17, W5)
Section titled “Team policy and structured audit log (0.17, W5)”A team policy sets limits that a project cannot loosen, and an audit log records the permission decisions:
- Team policy: guardrails that a project’s settings cannot loosen. Garuda reads the managed file
(
/Library/Application Support/Garuda/policy.jsonon macOS,/etc/garuda/policy.jsonon Linux; an admin writes it) and~/.garuda/policy.json. Where both exist, the stricter value wins: lists of denials add up,requireSandbox,strictAllowlistand the strictprofileare on when either sets them, limits take the lower value, andallowedModelsandauditcome from the managed file when it sets them. A broken file stops Garuda. A project’s own.garuda/policy.jsonis ignored with a notice: a cloned repo could otherwise remove its own limits. The chat,-p, jobs, the night shift andgaruda evalall use the policy./auditnames the files. The keys:disallowedCommands: command patterns that are denied in every run (for example["rm -rf *", "git push *--force*"]). A compound command is denied when one of its parts matches.requireSandbox: Whentrue, no command runs outside the OS sandbox:outside_sandboxis denied, and on a machine with no OS sandbox every command is denied.profile(0.17):"strict"requires the OS sandbox. Without one, Garuda stops at startup and runs no command on the host;"executor": "host"is an error; thebashtool has nooutside_sandbox. Strict wins in a merge. A project’s.garuda/settings.jsoncan also set"profile": "strict", but cannot turn off a policy’s strict profile.denyPaths: root-relative path patterns that the agent may not read or write (for example["**/.env*", "secret/**"]). File tools refuse them;grep,globand the code index skip them (no content, no name). Commands in the OS sandbox cannot read or write them either: the matching files and folders are added to the sandbox profile when each command starts (every folder is searched, alsonode_modulesand.git, so a large tree makes each command start slower). When the patterns match more than 1,000 paths, commands do not run, with a message: Garuda never leaves a denied path without a rule. A symbolic link to a denied file is denied too.allowedModels: Allowlist of LLM models permitted for use in the organization.network:blockedHosts(*.example.comfor subdomains) andstrictAllowlist. A blocked host stays blocked forweb_fetchand for sandboxed commands, also when the project’snetwork.allowlists it.limits: Global caps onmaxStepsandtokenBudget. They hold for each subagent run too (explore, custom agents, MoE specialists), whatever thesubagentsormoesettings say (0.14).
- Structured audit log (
~/.garuda/audit/<project>-<hash>/, one file per Garuda process): on in the chat,-pand jobs (a job writes under its main checkout’s folder, so the log outlives the worktree); off in evals and tests; the team policy can turn it off (audit.enabled: false) or make it mandatory (audit.enabled: true: a failed write then fails the call with an error result, and says whether the call ran). Without a policy a failed write gives one notice. It is not in the project, so the agent’s own tools cannot edit it. Targets and reasons pass the session redactor, so known secret formats (keys, tokens,password=…) are removed. Each line hasseq,prevandhash(sha256):/audit verifyshows a changed, removed or inserted line. With"audit": { "checkpoint": true }in the policy (0.17, off by default), Garuda also keeps each file’s lastseqandhashincheckpoints/<file>.json, and/audit verifythen shows lines cut from the end of a file and a deleted file. This makes the log tamper-evident, not tamper-proof: a user (or a program that runs as the user) can rewrite a file and its checkpoint together. Sandboxed commands cannot write~/.garuda. Each line records:- Every authorization decision (
allow_readonly,allow_sandbox,deny_policy,deny_user, etc.). - Security risk classification (
low,medium,high,critical). - Tool execution duration in milliseconds and error status, also for the tool calls of a subagent (0.14).
- Calls that a preToolUse hook blocked, and each hook command that ran (0.14). The team policy’s
requireSandboxanddisallowedCommandshold for hook commands too.
- Every authorization decision (
- Chat command:
/auditdisplays active policy rules and recent audit events. Use/audit statsfor totals,/audit denialsfor security blocks,/audit verifyto check the hash chains, or/audit <n>for recent events.
Sessions, limits and context
Section titled “Sessions, limits and context”Every run writes a session file: .garuda/sessions/<id>.jsonl (one JSON record per line, mode 0600).
It holds every message, tool call, result, token count and cost. Garuda redacts known secret
formats (API keys, tokens, private keys, password=…) and the values of secret-looking environment
variables before it writes a line. The model can still see a secret; the file never holds it.
garuda -p "Find the flaky test" # new sessiongaruda --resume -p "Now fix it" # continue the latest sessiongaruda --resume 20260923-201500-a1b2 -p "…" # continue a given sessiongaruda --replay 20260923-201500-a1b2 # replay it: no API calls, no tools run- After each turn Garuda prints steps, tokens (with cache reads), cost, context use and the session total.
- A run stops at 50 steps, at the session token budget (20M tokens), or after the same tool call
3 times in a row. Garuda says why. The budget holds for the whole turn (0.17): each model call
reserves its request and its output limit first, and stops when they do not fit; subagents,
compaction and retries reserve in the same budget, and a subagent never takes the room that the
parent needs for its final answer. Every generation attempt reserves, also provider retries
and a batch job’s switch to the normal API. Any call without complete provider-reported usage
keeps its full reservation charged, even if the response succeeded, also after a resume.
Reported child expenditure is saved immediately,
including before cancellation or a parallel batch failure.
/compactcounts its full summary request too. Complete tool results, including errors and hook feedback, are at most 100 000 characters; subagent results are at most 20 000 including metadata. The final-answer floor allows for worst-case JSON escaping, so small budgets can refuse child work earlier. Every request estimate covers the full current system prompt, conversation, tool definitions and provider settings, including after a resume or model change. If the previous measured context plus new messages is larger, Garuda keeps that allowance. This can refuse a call earlier. Missing, partial or invalid OpenAI-compatible usage gets a labelled estimate for context sizing. Estimates are excluded from reported token totals and never added on top of the full charge./usageand the turn summary show the separate budget charge; the cost shows as a minimum, “≥ $X (incomplete)”, never as unknown. Providers that omit usage can therefore reach the budget sooner. JSON marks estimated assistant usage and incomplete result totals; eval reports show unknown tokens, means and cache share for these runs. - When the connection to the model breaks while a response streams, Garuda sends the request again (up to 2 times) and says so. The session keeps only the complete response.
- At 80% of the context window, Garuda first cuts long tool outputs in older turns. If that is not enough, the model summarises the older turns. The last 4 turns always stay in full.
AGENTS.md,CLAUDE.mdandGARUDA.mdin the project root go into the system prompt, in that order;GARUDA.mdwins on a conflict. A file with the same text as an earlier one goes in once.- Project memory: the
remembertool saves lasting facts (build and test commands, layout, conventions) to.garuda/memory.md, with your approval. The next session loads them afterGARUDA.md. Edit or delete lines freely. Addremembertopermissions.allowto skip the question. read_filedoes not send the same lines of an unchanged file twice. After compaction it sends them again.- Garuda knows the price and context window of current Claude models. For other models, set them in settings.
Sandbox
Section titled “Sandbox”Garuda runs commands in an OS sandbox: Seatbelt (sandbox-exec) on macOS, bubblewrap (bwrap) on Linux.
In the sandbox, a command:
- can read every file, except secrets in your home folder (
~/.ssh,~/.aws,~/.gnupg,~/.kube,~/.docker,~/.netrc,~/.npmrc,~/.git-credentials, keychains, Garuda’s and Claude Code’s tokens and more); - can write only in the working root, the temp folders and package caches (
~/.cache,~/.npm, pnpm, and for a Java or Python project~/.m2/repository,~/.gradle/cachesand similar); - cannot write
.git/hooks,.git/configor.garuda/in the root, because those run or apply later outside the sandbox; - has no network (localhost works on macOS), unless you open a network allowlist (0.13, below);
- cannot start apps on macOS (
open,osascript), because an app started that way runs outside the sandbox (0.14); - sees only its own processes on Linux, not Garuda’s or yours (0.14).
Commands in the sandbox run with no approval. Deny rules still apply. When the sandbox blocks a command
(for example pnpm install needs the network), the model can ask to run it outside the sandbox.
That asks you first, unless one of your allow rules covers the command, and the prompt says
“OUTSIDE the sandbox”. The strict profile forbids it.
Settings:
{ "executor": "auto", "sandbox": { "writePaths": ["~/tools/cache"], "denyRead": ["~/work-secrets"] }}executor:auto(default) uses the sandbox when it works on this machine, else runs on the host with a notice.osrequires the sandbox.hostturns it off, and every command asks again (unless an allow rule covers it).sandbox.writePathsandsandbox.denyReadadd paths.~/is the home folder; other relative paths start at the root.- macOS sandbox probe (0.14): at startup Garuda runs
truethrough/usr/bin/sandbox-execonce. When that fails (for example in a nested sandbox),autofalls back to the host with a notice that gives the first line of the error. - On Linux, install bubblewrap (
sudo apt install bubblewrap). Some systems block the user namespaces that it needs; Garuda then falls back to the host and says why.
Staged commands (0.17, off by default)
Section titled “Staged commands (0.17, off by default)”With "commands": { "mode": "staged" } in .garuda/settings.json, a command in the sandbox cannot
change your project without your answer:
- At the session start, Garuda makes a staging copy of the project in
~/.garuda/staging/(a clone on APFS, a reflink on Btrfs or XFS, else a plain copy of at most 2 GB). It is removed at the end. - Each sandboxed command runs in the copy. On Linux the copy is mounted at the project’s path; on macOS the command runs at the copy’s path, and the project is read-only for it.
- After the command, Garuda lists what changed. No change that git tracks: no question. A change: the
question shows the files and the diff, and you apply or discard. Apply copies the files into the
project (all or nothing;
/undotakes them back). The model gets a note with your answer. - Files that
.gitignorecovers (build output,node_modules) stay in the copy for the next commands and are never applied. Changes in.gitare not applied either. The question counts them per folder and names sensitive ones (for example.env). - A file that you changed in the project during the command is a conflict: nothing is applied. A link that points out of the project is refused. A list that is not complete (more than 50,000 files) can only be discarded.
- No copy (no OS sandbox, a project over the limits): the session does not start. Garuda never runs the commands in direct mode instead.
- Not staged:
outside_sandbox: true(it asks first, as before), MCP servers and language servers (long-running programs), and scheduled jobs (they have their own worktree). Background daemons are off in staged mode. A hook that writes files asks like a command.
Network allowlist (0.13)
Section titled “Network allowlist (0.13)”Installs and builds need a package registry, but not the whole internet. The allowlist lets commands in the sandbox reach named hosts through Garuda’s own proxy, with no question:
{ "network": { "allow": ["npm", "pypi", "api.example.com", "*.example.org"] } }- Presets:
npm,pypi,maven,go,cargo,github(each names its registry hosts). A host with*.matches its subdomains. Only ports 80 and 443. - Off by default. The first turn shows the list and asks; “remember” pins it to the list in
~/.garuda/trust.json, so a changed list asks again. The model then gets a note that names the hosts, so it runs those commands in the sandbox and does not ask to leave it. - A host that is not on the list asks you (once, this session, or no);
network(host)inpermissions.alloworpermissions.denyanswers for good. Jobs and evals deny it and say which host. - Commands get
HTTP_PROXYandHTTPS_PROXY: npm, pnpm, yarn, pip, cargo, go, git and curl use them; Node’sfetchtoo (NODE_USE_ENV_PROXY, Node 24 or later); Maven and Gradle throughMAVEN_OPTSandGRADLE_OPTS. A tool that ignores the proxy variables still has no network. - The proxy does not read the traffic: for https it sees only the host name. A host must resolve to public addresses; the proxy connects to the address it checked.
- On Linux the proxy reaches bubblewrap’s network namespace through a small bridge that runs with
node; with nonodeon the PATH the allowlist stays off and Garuda says so. - A job keeps the list that was in effect when you scheduled it (the approval shows it).
garuda eval --network npm,pypimeasures a suite with the list. - Limit on Linux: a protected path that does not exist yet (for example
.git/hooksin a folder with no.git) is not protected.
Background daemons (0.14, off by default)
Section titled “Background daemons (0.14, off by default)”Dev servers and watchers can run in the background. This is off by default (no measured gain yet).
Turn it on with "daemons": { "enabled": true } in .garuda/settings.json.
- With the setting on,
bashtakesis_daemon: true: the command starts through the same executor (the same sandbox and approval as any command) and the tool returns at once with adaemonId. - The
process_managertool haslist,status,logs(lines,stream) andkillfor daemons that Garuda started in this session.list,statusandlogsrun without a question;killasks (or follows a rule) and is refused in plan mode. - Logs are kept in memory: the newest 2,000 lines per daemon, at most 4,000 characters per line, and
logsreturns at most 30,000 characters (the newest lines). - When the session closes, Garuda stops every running daemon (SIGTERM to the process group, then SIGKILL).
Custom agents
Section titled “Custom agents”A custom agent is a Markdown file in Claude Code’s subagent format. Garuda reads your Claude Code agents where they are.
<!-- ~/.garuda/agents/test-writer.md (or ~/.claude/agents, .garuda/agents, .claude/agents) -->---name: test-writerdescription: Writes unit tests for a module. Use when the user asks for tests.tools: Read, Grep, Glob, Write, Bashmodel: haiku---You write focused unit tests with the project's test tool. Run them before you answer.- The model hands a task to an agent with the
agenttool. The agent works in its own context with its own instructions and tools, and returns a short report. Agents cannot start agents. - Without
tools, an agent can only read. Named write tools and bash still ask or run in the sandbox, and plan mode holds. An agent that may write never runs at the same time as another call. model:haiku,sonnet,opus, or a model id (your own agents only). Default:--subagent-model, else the main model. Its tokens and cost count in the session.- A project agent shows its tools and instructions and asks the first time. Your own agents win over project agents with the same name (in Claude Code, the project wins).
/agentslists them. Nothing changes without agent files;"agents": { "enabled": false }turns them off. Details: docs/lld/agents.md.
Explore subagent
Section titled “Explore subagent”Off by default. Turn it on with "subagents": { "enabled": true } in .garuda/settings.json.
An A/B eval on the hard suite showed no gain in steps or cost (details in docs/lld/agents.md); it may
help in long chats on large repositories.
For an open question about the code (“where is the coupon applied, and who calls it?”), the model can
call explore. A subagent searches with glob, grep and read_file in its own context and returns a
short answer with path:line references. The main context stays small, and several explore calls can
run at once.
- The subagent can only read. It uses the same permissions and hooks, so secrets stay blocked.
- Limits per call: 20 steps and 150 000 tokens. When it hits a limit, it still answers with what it found, and says so.
- It uses the main model.
--subagent-model <spec>(orGARUDA_SUBAGENT_MODEL) picks another one, for exampleclaude-haiku-4-5under Sonnet. Its tokens and cost count in the session totals. - The chat shows one live line per call (
explore … · step 3 · grep /coupon/); Ctrl-O shows the answer and what it searched. Each run has its own file in.garuda/sessions/<session id>/. - Settings:
"subagents": { "enabled": true, "maxSteps": 20, "tokenBudget": 150000 }.
Java and Python projects
Section titled “Java and Python projects”Garuda finds the build tool from files in the working root and tells the model how to build and test.
The banner shows what it found, for example Java (Maven).
| Found | Test command | The sandbox may also write |
|---|---|---|
pom.xml |
mvn -B -q -o test (or ./mvnw) |
~/.m2/repository, ~/.m2/wrapper |
build.gradle(.kts), settings.gradle(.kts) |
gradle test --offline -q (or ./gradlew) |
~/.gradle/caches, wrapper, daemon and other cache folders |
pyproject.toml, setup.py, requirements.txt … |
python3 -m pytest -q (or the project’s .venv, uv run, poetry run) |
~/.local/share/uv |
- The commands run offline, because the sandbox has no network. When a dependency is missing, the model asks to run the download outside the sandbox, and you approve it.
- Settings files and init scripts (
~/.m2/settings.xml,~/.gradle/init.d,gradle.properties) stay read-only: the build tool runs them later, outside the sandbox. GARUDA.mdcan override the detected commands. Gradle in the sandbox is not tested yet.
MCP servers
Section titled “MCP servers”Garuda connects to local MCP servers over stdio (0.2) and to remote servers over Streamable HTTP, with OAuth sign-in (0.4).
{ "servers": { "github": { "command": "github-mcp-server", "args": ["stdio"], "env": { "GITHUB_PERSONAL_ACCESS_TOKEN": "${GITHUB_TOKEN}" }, "network": true } }}- Your own servers go in
~/.garuda/mcp.json. A project can add servers in.garuda/mcp.json. - Keys per server:
command,args,env,network(default false),writePaths,timeoutMs(default 60000),enabled.${NAME}in an env value comes from your environment, so secrets stay out of the file. - Servers start with the first task.
/mcpshows their state. - A remote server needs only its URL:
"linear": { "url": "https://mcp.linear.app/mcp" }. When it asks for a sign-in, Garuda asks you, opens your browser, and keeps the tokens in~/.garuda/mcp-auth.json(only you can read it). Tokens refresh by themselves;/mcp logout <server>removes them.
Security:
- A project server can come from a cloned repo, so Garuda asks before it starts one. The question
shows the full command, the sandbox, the network setting, the env variables and warnings
(npx, shell commands, secrets). “Remember” pins your answer to a hash of the definition in
~/.garuda/trust.json; any change asks again. A project cannot replace one of your own servers. - A remote server is not in the sandbox: it runs on another computer and gets the arguments of its tool calls. A project’s remote server needs your consent (pinned to its URL), must use https and a public address, and Garuda checks the address at each connection.
- Each local server runs in the OS sandbox, like bash: it can write only in the project and temp folders,
cannot read
~/.sshand other secrets, and has no network unless"network": true. It gets only the normal environment variables and the ones in itsenv. - Garuda remembers each server’s tools (a hash per tool). When they change later (a “rug pull”), Garuda warns you and says which tools were added, removed or changed, with the new descriptions. A project server needs your consent again.
- When a configured server is not available (you said no, it failed or it stopped), Garuda tells
the model once in a
<garuda_note>, so the model says so instead of guessing. Server text cannot pose as such a note or close its<mcp_result>wrapper. - An MCP tool call asks for approval and shows its arguments, unless an allow rule covers it. Server hints such as
readOnlyHintare shown, never trusted. Allow rules skip the question:mcp__github__get_issueormcp__github__*. - Tool names get the prefix
mcp__<server>__, so a server cannot replace a Garuda tool. - Descriptions and results are cleaned (terminal escape codes, control, invisible and Unicode tag
characters), limited in size, and marked as untrusted for the model (
<mcp_result>). - Garuda offers no sampling, roots or elicitation, so a server cannot make model calls or ask you questions through Garuda.
- The evals never start MCP servers.
Web fetch
Section titled “Web fetch”web_fetch reads a web page (http or https) and gives it to the model as Markdown (0.2).
- The first fetch from a host asks you and shows the full URL. “Yes, allow
for this session” skips later questions for that host. Rules skip the question for good: web_fetch(docs.python.org), orweb_fetch(*.github.com)for its subdomains. - An unusual URL (very long, or with a long token that could carry data from the session) always asks, also for an allowed host.
- A redirect to another host asks like a new fetch.
- Protection against server-side request forgery: Garuda resolves each host itself, checks every address (only public addresses; no private, loopback, link-local such as 169.254.169.254, or other special ranges, also as IPv4-mapped IPv6), and connects to exactly the checked address. Redirects are followed by hand (at most 5) and checked the same way.
- http is upgraded to https. URLs with a user name or password are refused. Garuda sends no cookies and no credentials, and it does not use a proxy.
- Limits: 5 MB per page (also after decompression), 30 s, text types only. HTML becomes Markdown
without scripts, styles and navigation. Long pages come in parts (
start,max_chars); a page is cached for 10 minutes. - Page text is cleaned like MCP text and marked as untrusted (
<web_result>). - Settings:
"web": { "enabled": false }removes the tool."web": { "allowLocalhost": true }allows loopback addresses (for a local docs server); private network ranges stay blocked. - The evals turn web_fetch off.
Web search
Section titled “Web search”web_search finds pages for a query (0.5). Set a key and it appears:
export BRAVE_API_KEY=... # Brave Search, or:export TAVILY_API_KEY=... # TavilyOr pick a backend in ~/.garuda/search.json: { "provider": "brave" }, { "provider": "tavily" }, or
your own SearXNG server: { "provider": "searxng", "url": "http://localhost:8888" } (turn on its JSON
format). Keys come only from environment variables; a project cannot configure search.
- The query leaves your machine, so each search shows the query and asks. “Yes, for this session” skips
the question for later searches;
"permissions": { "allow": ["web_search"] }skips it for good (and allows searches in plan mode). - A query with a long token (it could be a key) is refused.
- Results are titles, URLs and short texts, marked as untrusted. The model reads pages with
web_fetch. "web": { "enabled": false }removes both web tools. Details: docs/lld/web.md.
Claude’s web search (0.6)
Section titled “Claude’s web search (0.6)”With a Claude model, the model can use Claude’s own search tool, which runs on Anthropic’s servers. Turn it
on with a claude section in ~/.garuda/search.json:
{ "claude": { "maxUses": 5 } }{ "provider": "tavily", "claude": { "blockedDomains": ["example.com"] } }- The search runs inside the model’s reply, so Garuda cannot ask before each query. Choose once which
web search you want, with
"use"in~/.garuda/search.json:"claude"(Claude’s search; your other backend stays the fallback for other models),"provider"(only your other backend) or"off".garuda initasks it when Claude’s search is set up and nothing is saved; without a saved choice, the chat asks once before the first task and saves the answer. No session asks again. /searchshows what is in use./search claude|provider|offswitches for this session;/search default claude|provider|offchanges the saved choice.- It costs $10 per 1,000 searches, on top of the tokens. The usage line and
/usageinclude it. maxUses(1–20, default 5) caps the searches per model request.allowedDomainsorblockedDomains(not both) limit the sites.- Your other backend (
provider, or a key in the environment) is the fallback: for a model that is not a Claude model (for example after/models), or with"use": "provider". Without one,provideris not offered. - Custom agents whose tools allow
WebSearch(orweb_search) get it too, in the same session. - The chat shows each search:
● web_search (Claude) <query>and the number of results; Ctrl-O lists the pages.
Scheduled jobs (0.7)
Section titled “Scheduled jobs (0.7)”A plan can run later, with nobody at the keyboard: for example overnight.
- Make a plan in plan mode (
/plan, then your task). Each plan now ends with apermissionsblock: the file edits and network commands the build needs. - At “Build this plan?”, choose “No, keep planning”, and type
/schedule 01:00(the time is optional). Garuda shows the approval list, where the job starts (your last commit, plus your uncommitted changes of tracked files) and its branch, and asks once. - In a terminal in the project folder:
garuda run <job-id> --at 01:00. It waits, then builds the plan in its own worktree on the branchgaruda/job-<id>. Your checkout does not change.
- The job asks nothing. Reads, commands in the sandbox and the calls in its list are allowed; any other call
is denied, and the job goes on and reports it. You can edit the list (
"allow") in.garuda/jobs/<id>.jsonbefore the run. - The write scope (0.17):
/schedulealso proposes"writeScope", the paths in the worktree that the job may change: the folders and files of the plan’sedit_fileandwrite_filerules, and the build folders of the project (target/,build/,dist/…). The question shows it. The OS sandbox makes the rest of the worktree read-only for the job’s commands, the engine denies edits outside it, and a change outside it fails the job with no commit. A rule with no folder (edit_file(**/*.ts)) gives no scope: the question then says that commands may write anywhere in the worktree. Jobs made before 0.17 have no scope. The scope holds for every command of the job, also its test command (proof of work), and a job never runs a command outside the sandbox, whatever its rules say. A test that writes a file outside the scope (for examplecoverage.txtin the root) fails; add a folder for it to"writeScope". An edit through a symbolic link must reach a path in the scope too. - A job needs git and the OS sandbox.
node_modulesand.venvare linked from your checkout. - At the end Garuda commits the changes on the job branch (no git hooks run) and writes a report:
.garuda/jobs/<id>.md, also shown by/jobs <id>. Review withgit diff, thengit merge garuda/job-<id>, and remove the worktree. /jobslists the jobs. Keep the Mac awake and on power while the terminal waits./jobs delete <id>(0.11) asks, then removes the job file, report, log and worktree. An unmerged job branch may hold work: you choose to keep it or delete it too. A running job cannot be deleted.- On macOS,
/schedule HH:MMoffers a launchd agent: the job then starts at that time also with no terminal open. It runs through your login shell (so your.zshrcgives it the API keys) undercaffeinate, logs to.garuda/jobs/<id>.log, and sends a macOS notification at the end. If the Mac sleeps at that time, the job starts at the next wake (sudo pmset schedule wake "<date>"can wake it)./jobs cancel <id>removes the agent. - With a Claude model,
/schedulealso asks whether the job should use the Batch API: half the token price, but each step waits for its batch: about 3 minutes in the first measurement, sometimes hours. A step that waits more than 20 minutes ("stepLimitMinutes") runs on the normal API, and the next step tries the Batch API again. 15 minutes before the finish-by time ("finishBy": "07:00"in the job file) a job that still runs goes on with the normal API. The run prints the batch id and a line per minute of waiting.
The night shift (0.11)
Section titled “The night shift (0.11)”Each /schedule also puts the job in the project’s night queue. garuda night --at 01:00 waits, then
runs the queued jobs, up to 3 at a time (--parallel n, 1 to 10), each in its own worktree and with its
own log (.garuda/jobs/<id>.log). At the end it writes one digest, .garuda/jobs/night-<date>.md: a row
per job with its verdict, tests, files, cost and branch, and why the others need a look. /jobs shows
which jobs are in the queue. A job with its own launchd agent is not in the queue.
On macOS, /schedule HH:MM asks whether launchd should start the whole night queue at that time
(one agent per project, garuda night through your login shell under caffeinate, log
.garuda/jobs/night.log, a macOS notification at the end), only this job, or nothing. A later
/schedule HH:MM moves the queue’s agent to the new time. The agent removes itself after the run;
/jobs shows it, and /jobs cancel night removes it.
Proof of work (0.11)
Section titled “Proof of work (0.11)”Each job report starts with a verdict: Ready to merge or Needs a look. It rests on three things:
- Tests before and after. Garuda finds the test command (a
testscript inpackage.json, run with pnpm, yarn or npm by the lock file; else the Maven, Gradle or pytest command; elsenode --testwhen there are*.test.jsfiles) and runs it in the sandbox at the job’s base and again after the job."test"in the job file changes or sets it. - Risk flags. STOP: the job did not finish, or the tests fail after it. Look: no test command, test files deleted or changed, dependency or build files changed, CI or environment files changed, a large change, denied calls, tests that failed before and pass after.
- A review. The job’s model works as a staff engineer. A second request to the same model, as a
principal engineer who knows the project’s stack, reads the request, the plan, the diff, the test
results and the flags, and answers
VERDICT: ready | needs a lookwith findings. It costs one model call (shown in the report);"review": falsein the job file turns it off.
A STOP flag always means “Needs a look”; otherwise the review decides.
Hooks are your own commands that run around the agent’s tool calls (0.2).
{ "hooks": { "preToolUse": [ { "tools": ["bash(git push*)"], "command": "echo 'Pushes need a review first.' >&2; exit 2" } ], "postToolUse": [ { "tools": ["edit_file", "write_file"], "command": "npx eslint \"$GARUDA_FILE\" >&2 || exit 2" } ] }}- Your own hooks go in
~/.garuda/hooks.json. A project can add hooks in.garuda/hooks.json; Garuda shows every command and asks first, like for MCP servers. “Remember” pins the answer to a hash of the hooks; any change asks again. toolsuses the permission rule syntax (edit_file,bash(git push*),mcp__github__*). An empty list matches every tool. Keys:command,tools,timeoutMs(default 30000),network.preToolUseruns before the approval question. Exit 0 lets the call go on. Exit 2 blocks it, and stderr tells the model why. Any other failure (another exit code, a timeout) also blocks the call: a broken guard never lets a call through. A hook can block a call, never approve one.postToolUseruns after the call. Exit 2 adds stderr to the result as feedback for the model (for example lint errors). Other failures only show a warning.- Hooks run in the OS sandbox, with no network unless
"network": true. They get$GARUDA_HOOK_EVENT,$GARUDA_TOOL,$GARUDA_FILE(for file tools),$GARUDA_COMMAND(bash),$GARUDA_URL(web_fetch), and$GARUDA_HOOK_INPUT: a JSON file with the tool input (and the result for postToolUse). /hooksshows the active hooks. The evals run with no hooks.
Permissions
Section titled “Permissions”Put rules in .garuda/settings.json in the project. Deny rules always win.
{ "executor": "auto", "permissions": { "allow": ["bash(pnpm test*)", "bash(git status)", "edit_file(src/**)"], "deny": ["bash(rm -rf*)", "bash(git push*)"] }, "env": { "allow": ["NODE_ENV"] }, "limits": { "maxSteps": 50, "tokenBudget": 20000000 }, "model": { "contextWindow": 200000, "price": { "input": 3, "output": 15, "cacheRead": 0.3, "cacheWrite": 3.75 } }}- A rule is
tool(every call) ortool(pattern). - File patterns are globs relative to the root. A name without
/matches at any depth. - Command patterns use
*for any text. Garuda splits a command at;,&&,||,|and$(…). A deny rule blocks the command when one part matches, also behindsudo,envorVAR=value. An allow rule must match every part as written:bash(pnpm test*)does not coverNODE_OPTIONS=… pnpm testorsudo pnpm test. - Sensitive files (
.env*, keys,.npmrc,.aws/, …) are blocked, also for reads, also through a symbolic link with another name. On macOS and Windows, path patterns ignore letter case (.ENVis.envthere). An allow rule that names the file, for exampleread_file(.env.example), unblocks it. - Write tools never change files in
.git/. - Commands see only these environment variables:
PATH,HOME,USER,LOGNAME,SHELL,TERM,LANG,LC_ALL,LC_CTYPE,TMPDIR,TZ, plus the names inenv.allow. API keys stay out. - With no terminal on stdin (a pipe or CI), Garuda cannot ask, so it denies calls that need approval.
- The project’s file comes with the repository, so its parts that loosen safety need your yes first:
executor: "host",permissions.allow,sandbox.writePaths,env.allowandweb.allowLocalhost. At startup the chat (and-pin a terminal) shows them and asks: yes for this run, yes and remember (pinned in~/.garuda/trust.json; a change asks again), or no. Without a yes, Garuda leaves those parts out and says so; the rest of the file (deny rules, limits, features) applies. A scheduled job uses them only when they are pinned. Settings thatgaruda initwrites ask once at the next start too.
Standalone binary
Section titled “Standalone binary”pnpm package builds one executable file, bin/garuda, for the machine you build on.
It holds Node and all of Garuda, so the target machine does not need Node.
It needs Node 25.5 or later to build (it uses node --build-sea).
pnpm package./bin/garuda # greeting./bin/garuda --versionsudo cp bin/garuda /usr/local/bin/ # optional: put it on your PATHNotes:
- The binary is about 100–150 MB, because it contains the Node runtime.
- Build on each platform that you want to run on. A binary built on an Apple silicon Mac runs only on Apple silicon Macs.
- On macOS, the script signs the binary ad hoc (
codesign --sign -), so it runs on the machine that built it. - Homebrew’s Node has single executables turned off. The script then downloads the official Node build of
the same version from nodejs.org, checks its SHA-256 sum and keeps it in
~/.cache/garuda. SetGARUDA_SEA_NODE=/path/to/nodeto use a Node binary of your choice, orNODEJS_ORG_MIRRORto use a mirror.
Layout
Section titled “Layout”| Folder | Holds |
|---|---|
src/model/ |
Provider-neutral types, ModelClient, the Anthropic adapter, the fake model |
src/loop/ |
runAgent(session, deps): the agent loop, limits, loop guard; replaySession |
src/context/ |
Compaction, the system prompt, and the loader of AGENTS.md, CLAUDE.md and GARUDA.md |
src/commands/ |
Custom slash commands |
src/tools/ |
Tool<I, O>, the registry, and the tools: read_file, glob, grep, write_file, edit_file, bash, process_manager |
src/permissions/ |
Path guard (F15), rules, sensitive paths, settings, and the permission engine (F17–F20) |
src/sandbox/ |
Executor interface, ExecPolicy, HostExecutor, SeatbeltExecutor, BwrapExecutor, DaemonManager. The only place that starts processes (N8) |
src/session/ |
Session state, records, SessionStore (JSONL files), resume, redaction, read tracking |
src/app/ |
Runtime: settings, executor, permissions and session for one process. The CLI and the evals share it |
src/cli/ |
Entry point, chat mode, renderer, terminal approver, garuda eval |
src/evals/ |
The eval suites (basic, hard, java, python), the shopkit repo generator, toolchain checks and the runner |
src/agents/ |
Subagent execution (child.ts): the explore subagent (0.3), custom agents (0.5), and Mixture-of-Experts language specialists (0.17) |
src/lang/ |
Language profiles: build and test commands, prompt notes and sandbox caches for Maven, Gradle and Python projects |
src/knowledge/ |
The code index: KnowledgeIndex, LanguageExpert, the TypeScript/JavaScript expert |
scripts/ |
Build helpers. package.mjs makes the standalone binary |
test/ |
Vitest suites. loop.test.ts, m2.acceptance.test.ts and m3.acceptance.test.ts, m4.acceptance.test.ts, m5.acceptance.test.ts hold the milestone acceptance tests. executorContract.ts is the suite every executor must pass |
- The loop gets all dependencies as arguments. It never imports the CLI.
- Only
src/model/anthropic.tsimports the Anthropic SDK (N1). - Only
src/sandbox/may start processes (N8). Biome and a test enforce this. - Strict TypeScript. No
anyin public interfaces (N7).
Contributions and CI
Section titled “Contributions and CI”Every pull request runs .github/workflows/ci.yml: pnpm check and a build on Linux (with
bubblewrap, so the sandbox tests run) and macOS (Seatbelt), and a gitleaks scan of the history. The
jobs have read-only access and get no secrets. Changes to main need a pull request and a review by
the code owner (.github/CODEOWNERS).
The website lives in site/ (Astro and Starlight). Its docs pages are generated from docs/ and this
README at build time (site/scripts/sync-docs.mjs), so edit those files, not the generated pages.
.github/workflows/pages.yml builds the site for each pull request that changes it, and deploys it
to GitHub Pages from main. To work on it: cd site && pnpm install --ignore-workspace && pnpm dev.
A tag v<version> runs .github/workflows/release.yml, which publishes the npm package with npm
trusted publishing (no token in the repository). The steps, the first publish and the Homebrew tap
are in docs/release.md.
License
Section titled “License”Garuda is licensed under the Apache License, Version 2.0. See NOTICE. To report a security problem, see SECURITY.md.