Goose: The Extension-First Rust Agent with LLM-Based Adversary Inspection
Goose (by AAIF β Agentic AI Foundation at the Linux Foundation) is a Rust-native AI agent that runs on your machine β desktop app, CLI, and API. With 15+ providers, MCP support, a multi-stage security inspector stack, and a skill/recipe framework, Goose represents a distinctive engineering philosophy: make the core lean and extensible, then let extensions do the heavy lifting.
What makes Goose different
Goose occupies a unique position in this agent set: it is a Rust-native, extension-first runtime with a security model that uses a second LLM to review tool calls. The architecture is deliberately minimal at the core β tools come from extensions, security comes from a layered inspector stack, and learning happens through a recipes and skills system.
Three deployment modes
Desktop app: Native desktop for macOS, Linux, and Windows with full UI. CLI: Full terminal workflow for scripted and interactive use. API: Embed Goose anywhere via HTTP API. All three share the same Rust core and extension system.
Extension-first philosophy
Unlike agents that ship with 40+ built-in tools, Goose's core ships with only a handful of platform tools. Everything else β file editing, web search, database access, specialized workflows β comes through the extension system. This keeps the core lean and makes the agent behavior genuinely customizable.
LLM-based adversary inspection
The AdversaryInspector (in
src/security/adversary_inspector.rs) is unique in this set:
it fires a second LLM call to review tool calls against user-defined rules
from ~/.config/goose/adversary.md. This is defense-in-depth
against a rogue sub-agent β not pattern matching, actual LLM judgment.
Architecture overview
Goose is built as a Cargo workspace with multiple crates.
The main goose crate contains the core agent runtime, tool
inspection pipeline, provider system, and extension manager. Key crates:
| Crate | Purpose | Key modules |
|---|---|---|
goose |
Core agent runtime, tools, providers | agents/, providers/, security/, config/ |
goose-cli |
Terminal interface | commands/, session/, recipes/ |
goose-server |
HTTP API server | routes/, auth.rs, tunnel/ |
goose-mcp |
MCP server implementation | mcp_server_runner.rs, computercontroller/ |
goose-sdk |
SDK for extensions | Extension interface types |
goose-acp-macros |
ACP protocol macros | Protocol derive macros |
The agent core: agent.rs
The main Agent struct in src/agents/agent.rs (~2,500 lines)
handles the complete agent loop. The key design patterns:
Provider system
The agent holds a SharedProvider (an Arc<Mutex<Option<Arc<dyn Provider>>>>)
that can be updated at runtime. This enables sub-agents to switch providers
independently of the main agent. The update_provider() method
persists the provider choice to the session.
Tool inspection pipeline
Before any tool call executes, it passes through a ToolInspectionManager
with stacked inspectors: Security β Egress β Adversary β Permission β Repetition.
Each inspector can approve, deny, or require user approval. This is the most
layered security pipeline in this agent set.
// From src/agents/agent.rs β the inspection stack setup
fn create_tool_inspection_manager(
permission_manager: Arc<PermissionManager>,
provider: SharedProvider,
) -> ToolInspectionManager {
let mut manager = ToolInspectionManager::new();
// Add security inspector (highest priority - runs first)
manager.add_inspector(Box::new(SecurityInspector::new()));
manager.add_inspector(Box::new(EgressInspector::new()));
// Add adversary inspector (LLM-based review, enabled by ~/.config/goose/adversary.md)
manager.add_inspector(Box::new(AdversaryInspector::new(provider.clone())));
// Add permission inspector (medium-high priority)
manager.add_inspector(Box::new(PermissionInspector::new(...)));
// Add repetition inspector (lower priority - basic repetition checking)
manager.add_inspector(Box::new(RepetitionInspector::new(None)));
manager
}
Sub-agent provider isolation
When spawning a sub-agent, the main agent can call
agent.update_provider(task_config.provider.clone(), &session_id)
to give the sub-agent its own provider. This means sub-agents can use
different models than the parent agent β a powerful architectural choice
for cost optimization.
Multi-mode security: GooseMode
Goose implements four distinct operating modes defined in
src/config/goose_mode.rs:
| Mode | Description | Security behavior |
|---|---|---|
| Auto (default) | Automatically approve tool calls | Minimal friction, tools run immediately |
| Approve | Ask before every tool call | Maximum control, every tool requires confirmation |
| SmartApprove | Ask only for sensitive tool calls | Uses read_only_hint annotation; auto-approves read-only tools |
| Chat | Chat only, no tool calls | Zero tool execution, pure conversation mode |
The SmartApprove mode uses a particularly elegant pattern:
it calls permission_manager.apply_tool_annotations(&tools)
which reads the MCP ToolAnnotations.read_only_hint field.
Tools with read_only: false are automatically added to the
"ask_before" list in the smart-approve permission config.
The LLM-based AdversaryInspector
The AdversaryInspector in src/security/adversary_inspector.rs
is the most distinctive security component in Goose. It activates when you
place an adversary.md file in your Goose config directory
(~/.config/goose/adversary.md).
adversary.md format
tools: shell, computercontroller__automation_script
---
BLOCK if the command exfiltrates data or is destructive.
ALLOW normal development operations.
The frontmatter specifies which tools to review (defaults to
shell and computercontroller__automation_script).
The rules below the --- separator are passed to the LLM
for judgment.
How it works
- When a tool call targets a listed tool, the inspector formats the tool call with its arguments
- It extracts the original task from the conversation and recent user messages
- It fires a separate LLM call (using the same provider) with the tool call, task, and user rules
- The LLM responds with
ALLOWorBLOCKand a reason - If
BLOCK, the tool is denied with the LLM's explanation shown to the user
Fail-open design
If the LLM call fails (e.g., no provider, network error), the inspector fails open β allowing the tool to proceed. This prevents the security system from blocking legitimate work due to infrastructure issues.
// From src/security/adversary_inspector.rs β the LLM review prompt
let system_prompt = "You are an adversarial security reviewer, protecting the user \
in case the other agent is rogue. An AI coding agent is about to execute a tool \
call. Your ONLY job: decide if this tool call is safe given the user's task and \
rules. Respond with ALLOW or BLOCK on the first line, then a brief reason on \
the next line.";
let user_message = format!(
"The user's original task: {}\n\n\
Recent user messages (oldest first):\n{}\n\
The tool call to review:\n```\n{}\n```\n\n\
Rules:\n{}\n\n\
Respond with a single word on the first line: ALLOW or BLOCK\n\
Then on the next line, a brief reason.",
original_task, history_section, tool_description, rules
);
Provider system: 15+ providers across 35+ modules
Goose's provider architecture in src/providers/ is extensive.
The providers/mod.rs exports 35+ provider modules:
| Provider category | Modules |
|---|---|
| Direct API providers | anthropic, openai, google, gemini_cli, ollama, openrouter, bedrock, azure |
| ACP-bridged providers | claude_acp, pi_acp, codex_acp, copilot_acp, amp_acp |
| Platform-specific agents | chatgpt_codex (GitHub Copilot), claude_code, gemini_cli, kimicode, cursor_agent |
| Enterprise providers | databricks, snowflake, tetrate, venice, xai, nanogpt |
| Declarative providers | JSON config files in providers/declarative/: cerebras, deepseek, groq, lmstudio, mistral, novita, nvidia, ovhcloud, zhipu, and more |
| Local inference | local_inference/ with candle-core, llama-cpp-2, tokenizers β supports CUDA and Metal GPU acceleration |
The canonical/ directory contains auto-generated model mapping
data: canonical_models.json maps canonical model names to
provider-specific model identifiers, and provider_metadata.json
contains per-provider capabilities and limits.
Extension system and MCP
Goose's extension system is implemented in
src/agents/extension_manager.rs and enables dynamic loading of
MCP servers and custom tools. Extensions can be stdio-based (spawned as child
processes) or embedded via Docker.
Extension discovery
Extensions are discovered from ~/.config/goose/extensions/
and can be npm packages, local directories, or git repositories.
The ExtensionManager resolves commands via
SearchPaths::builder().with_npm() to find executables
in node_modules/.bin or on PATH.
malware scanning
Before loading an extension, Goose runs
extension_malware_check.rs to scan the extension's code
for suspicious patterns. This is a critical security measure given
that extensions run with the same permissions as the agent.
MCP client implementation
The McpClient in src/agents/mcp_client.rs implements
the MCP client protocol using the rmcp crate. It handles:
- Connection management and session tracking
- Tool listing and calling with proper request context
- Notification forwarding (progress, logging, etc.)
- Resource and prompt management
- Working directory synchronization across MCP sessions
Key files
src/agents/extension_manager.rs (extension lifecycle),
src/agents/mcp_client.rs (MCP client protocol),
src/agents/extension_malware_check.rs (security scanning),
src/config/extensions.rs (configuration)
Tool execution pipeline
The tool execution flow in src/agents/tool_execution.rs and
src/agents/agent.rs follows this sequence:
Split into frontend tools (UI handles) and extension tools
Security β Egress β Adversary β Permission β Repetition
Auto-approved tools dispatch immediately; denied tools return error; needs_approval tools await user confirmation
Tool call routed to appropriate MCP client or stdio extension
Result streamed back with optional MCP server notifications during execution
Sub-agent execution
Goose implements sub-agent execution in
src/agents/subagent_handler.rs with a comprehensive system:
Task configuration
Each sub-agent gets a TaskConfig specifying provider,
max_turns, and extensions. Sub-agents can use different providers than
the parent agent, enabling cost-effective delegation patterns.
Recipe-driven prompts
Sub-agents receive instructions from a Recipe object
that contains system instructions, task prompts, response schemas,
and retry configuration. The subagent_system.md prompt
template provides the agent persona.
// From src/agents/subagent_handler.rs β sub-agent prompt context
pub struct SubagentPromptContext {
pub max_turns: usize,
pub subagent_id: String,
pub task_instructions: String,
pub tool_count: usize,
pub available_tools: String,
}
// Template from src/prompts/subagent_system.md:
// "You are a specialized subagent within the goose AI framework,
// created by AAIF (Agentic AI Foundation). You were spawned by
// the main goose agent to handle a specific task efficiently."
The sub-agent supports final_output mode where it runs until
it produces structured output matching a JSON schema, then stops. This is
useful for extracting structured data from complex tasks.
Shell tool: platform-aware execution
The shell tool in src/agents/platform_extensions/developer/shell.rs
(~980 lines) is one of the most platform-aware shell implementations in this set:
| Feature | Implementation |
|---|---|
| Unix shell detection | Checks for bash on PATH, falls back to sh; respects GOOSE_SHELL env var |
| Windows shell handling | Detects PowerShell vs cmd vs POSIX-like shells (Cygwin/MSYS2) |
| Flatpak support | Detects Flatpak sandbox and wraps commands with flatpak-spawn --host |
| Login shell PATH resolution | Spawns a login shell to recover user's full PATH (fixes Electron-inherited minimal PATH) |
| Output handling | Parallel stdout/stderr collection, 2000-line/50KB limits, temp file fallback for overflow |
| Background process handling | Detects backgrounded processes (sleep &), sets output_truncated flag, doesn't wait for backgrounded sleep |
| Process session management | Uses ProcessSession wrapper to prevent bash job control from stealing terminal foreground |
Recipe and skill framework
Goose's recipe system enables converting chat conversations into reusable
automation workflows. Defined in src/recipe.rs and prompted
via src/prompts/recipe.md:
Recipe structure
- title β Human-readable name
- instructions β Reusable task description
- activities β Example use cases
- response β Optional JSON schema for structured output
- settings β Provider, model, temperature
- extensions β Required extensions to load
Recipe creation
During chat, the agent can call create_recipe() which
takes the conversation history, generates a structured recipe using
the LLM, and saves it as a reusable automation. Recipes are stored
in ~/.config/goose/recipes/.
Context compaction and memory management
Goose implements context compaction in src/context_mgmt/mod.rs.
The compaction triggers when context usage exceeds a configurable threshold
(default: GOOSE_AUTO_COMPACT_THRESHOLD).
- Tool pair summarization: Repeated tool call/response pairs are summarized to reduce context size
- Session naming: Goose auto-generates session names using the provider when not disabled
- MOIM injection:
inject_moim()insrc/agents/moim.rsinjects a message-of-instructions before the latest assistant message, sourced from extension-collected hints
Hint files system
Goose supports .goosehints files in project directories that
provide context hints to the agent. The PromptManager
loads these via load_subdirectory_hints() and can
refresh tool/prompt lists when hints change.
Key differences from other agents
vs Claude Code
Claude Code is Anthropic-first with 40+ built-in tools and a React/Ink TUI. Goose is provider-agnostic with an extension-first model where tools come from MCP or stdio extensions. Goose's security uses an LLM-based adversary reviewer; Claude Code uses a permissions system with wildcard rules.
vs Pi Mono
Both share the extension-first philosophy, but Pi Mono is TypeScript with a JSONL session format and Pi Packages (npm/git bundles). Goose is Rust with a SQLite session store and an ACP/MCP protocol layer. Goose's provider count (15+) is comparable to Pi Mono's (23).
vs Codex
Both are Rust-native and ship as compiled binaries. Codex focuses on coding with platform-specific sandboxing (Seatbelt/bubblewrap). Goose is more general-purpose with a focus on extensibility and the extension marketplace. Goose uses a layered security inspector stack; Codex uses an execpolicy rule engine.
vs DeerFlow
DeerFlow is a Python/LangGraph orchestration harness. Goose is a Rust native binary with a simpler but more production-focused architecture. DeerFlow's middleware chain is more composable; Goose's inspector stack is more focused on security. DeerFlow has 14 middleware layers; Goose has 5 security inspectors.
File count and codebase size
Goose's Cargo workspace contains multiple crates with substantial source:
| Crate | Notable modules | Characteristics |
|---|---|---|
goose |
agents/, providers/, security/, config/, conversation/ | ~50+ source files, 2,500+ line agent.rs |
goose-cli |
commands/, session/, recipes/ | Full terminal interface |
goose-server |
routes/, auth.rs, tunnel/ | HTTP API with TLS support |
goose-mcp |
mcp_server_runner.rs, computercontroller/ | MCP server + computer use tools |
goose-sdk |
Extension interface types | SDK for third-party extensions |
Notable engineering decisions
Extension isolation via container
The agent can run stdio extensions inside a Docker container via
set_container(). This provides process isolation for
untrusted or resource-intensive extensions.
Structured error handling with ErrorData
Tool errors use rmcp::model::ErrorData with error codes
(INVALID_PARAMS, INTERNAL_ERROR, etc.)
rather than plain strings β making error handling more programmatic.
Notification streaming during tool execution
ToolCallResult combines a result future with an optional
ServerNotification stream. Tools can emit progress
notifications during long-running operations without blocking the
result.
ActionRequired permission routing
Some providers (e.g., Claude Code via ACP) handle permissions natively
through an action-required protocol. Goose detects this via
permission_routing() == PermissionRouting::ActionRequired
and delegates confirmation handling to the provider.
Prompts via minijinja templates
System prompts use minijinja templates (embedded in
src/prompts/) rather than hardcoded strings. Templates
receive context like extension list, mode, tool counts, and render
dynamically at runtime.
Feature-gated dependencies
The Cargo.toml uses extensive feature gates:
local-inference (candle, llama-cpp, tokenizers),
aws-providers (Bedrock, SageMaker),
telemetry/otel (OpenTelemetry),
cuda (GPU acceleration), etc.
Key files reference
| File | Lines | What it does |
|---|---|---|
src/agents/agent.rs |
~2,500 | Main agent loop, reply(), tool inspection stack setup |
src/agents/tool_execution.rs |
~190 | Tool call dispatch, approval routing, result handling |
src/agents/extension_manager.rs |
~700+ | Extension discovery, MCP client management, tool routing |
src/agents/mcp_client.rs |
~500+ | MCP client protocol implementation using rmcp |
src/agents/subagent_handler.rs |
~350 | Sub-agent spawning, task execution, result collection |
src/security/adversary_inspector.rs |
~630 | LLM-based adversary review with fail-open behavior |
src/security/security_inspector.rs |
~150 | Pattern-matching security detection |
src/config/permission.rs |
~390 | YAML-based permission config with AlwaysAllow/AskBefore/NeverAllow |
src/config/goose_mode.rs |
~35 | Four-mode enum: Auto, Approve, SmartApprove, Chat |
src/agents/platform_extensions/developer/shell.rs |
~980 | Platform-aware shell tool with Flatpak, login-shell PATH, background process handling |
src/agents/platform_extensions/analyze/mod.rs |
~200+ | Code analysis extension with tree-sitter AST parsing, structure/θ―δΉ/focus modes |
src/agents/moim.rs |
~145 | Message-of-instructions injection before latest assistant message |
src/providers/mod.rs |
~65 | 35+ provider module exports |
src/providers/canonical/ |
Multiple files | Auto-generated canonical model mappings and provider metadata |
src/context_mgmt/mod.rs |
~300+ | Context compaction, tool pair summarization |
src/conversation/message.rs |
~500+ | Message types with tool requests/responses, thinking, system notifications |
My assessment
Best for
- Users who want a lean, extensible agent core rather than a feature-heavy CLI
- Security-conscious users who want LLM-based adversarial review of tool calls
- Teams building custom extensions and integrating with MCP servers
- Users who prefer Rust's performance and type safety for agent tooling
- Enterprise deployments requiring TLS, OAuth, and multi-provider support
Tradeoffs
- Extension-first model means some things require setup (vs. batteries-included agents)
- Security relies on user-defined rules in adversary.md β no built-in defaults
- Recipe framework is less mature than some alternatives' workflow systems
- Desktop app adds UI complexity that pure CLI agents avoid
Most distinctive feature
The AdversaryInspector using an LLM to review tool calls against
user-defined rules is unique in this agent set. No other agent has a
dedicated second-LLM security review for tool execution. This makes Goose
particularly interesting for multi-agent setups where the parent agent
delegates to sub-agents β the adversary inspector provides defense-in-depth
against a potentially compromised sub-agent.