How Goose Slash Commands Work: A Complete Guide to Creating Custom Commands
Goose slash commands provide shortcuts that trigger either built‑in CLI functions or user‑defined recipes, with custom commands stored as SlashCommandMapping entries in the global config and resolved at runtime by loading YAML recipe files.
Goose, the open‑source AI agent framework from Block, supports extensible slash commands that streamline repetitive workflows. While built‑in commands like /exit and /prompt handle core interactions, the platform also allows you to create custom slash commands that bind unique trigger strings to reusable recipe files. Understanding how these commands are stored, resolved, and executed enables you to extend Goose with domain‑specific automation.
Architecture of Goose Slash Commands
Goose distinguishes between two command types: built‑in commands hard‑coded in the CLI parser, and custom commands that dynamically map strings to recipe files. Custom mappings persist in the global configuration under the key slash_commands.
Storage and Data Model
The data model for custom commands lives in crates/goose/src/slash_commands.rs. The SlashCommandMapping struct defines the relationship between a command trigger and its target file:
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SlashCommandMapping {
pub command: String, // e.g., "my-task"
pub recipe_path: String, // absolute or relative path to a .yaml recipe
}
The module provides two key functions for persistence:
list_commands()reads the vector from the globalConfig(Config::global().get_param) and falls back to an empty vector on error.save_slash_commands()writes the updated vector back to the same config key.
Runtime Resolution
When a user enters text beginning with /, the system normalizes the input by trimming the leading slash and converting it to lowercase. The resolve_slash_command function in crates/goose/src/slash_commands.rs then handles the lookup:
pub fn resolve_slash_command(command: &str) -> Option<Recipe> {
let recipe_path = get_recipe_for_command(command)?;
if !recipe_path.exists() { return None; }
let recipe_content = std::fs::read_to_string(&recipe_path).ok()?;
let recipe = Recipe::from_content(&recipe_content).ok()?;
Some(recipe)
}
If the mapped file exists and parses correctly, the function returns a Recipe object that the agent loop executes.
CLI Input Handling
The CLI input loop in crates/goose-cli/src/session/input.rs first checks for a leading / character. If present, it calls handle_slash_command:
match handle_slash_command(&input) {
Some(result) => Ok(result),
None => Ok(InputResult::Message(input.trim().to_string())),
}
The handle_slash_command function contains a match statement for every built‑in command (e.g., /exit, /prompt, /recipe). Critically, if the command is not matched, the function returns None, causing the caller to treat the input as a normal message. This fallback mechanism allows custom slash commands to pass through to the agent, where resolve_slash_command processes them later.
Creating Custom Slash Commands
Adding a custom command requires creating a recipe file and registering the mapping in the global configuration.
Step 1: Create a Recipe File
A recipe is a standard Goose YAML file defining steps the agent should execute. Create a file such as ./recipes/my_task.yaml:
name: My Task
description: Runs a custom workflow.
steps:
- name: Echo
type: bash
command: echo "Running my custom task"
Save this in your project directory or a shared recipes folder.
Step 2: Register the Command
You can register the mapping through three interfaces: programmatically via Rust, through the HTTP API, or by manually editing the configuration.
Programmatic Registration (Rust)
Use the set_recipe_slash_command function to bind a path to a command string:
use std::path::PathBuf;
use goose::slash_commands;
fn register() -> anyhow::Result<()> {
slash_commands::set_recipe_slash_command(
PathBuf::from("recipes/my_task.yaml"),
Some("/my-task".to_string()),
)?;
Ok(())
}
This function automatically removes any existing mapping for the given recipe path before adding the new pair, and persists the change to the global config.
HTTP API
The Goose server exposes a POST endpoint at /recipes/slash-command defined in crates/goose-server/src/routes/recipe.rs. Send a JSON payload:
curl -X POST https://your-goose-instance/api/recipes/slash-command \
-H "Content-Type: application/json" \
-d '{"file_path":"recipes/my_task.yaml","slash_command":"/my-task"}'
On success, the server updates the global configuration.
Manual Configuration
Edit the Goose configuration file directly (typically ~/.goose/config.json) and add the mapping to the slash_commands array:
{
"slash_commands": [
{ "command": "my-task", "recipe_path": "recipes/my_task.yaml" }
]
}
Note that the stored command name omits the leading slash, though users must include it when typing.
Step 3: Execution Flow
When a user types /my-task, the following occurs:
handle_slash_commandreturnsNonebecausemy-taskis not a built‑in command.- The session treats the text as a message and passes it to the agent loop in
crates/goose/src/agents/execute_commands.rs. - The agent calls
slash_commands::resolve_slash_command("/my-task"). - The recipe loads and executes, producing the defined output.
Adding New Built‑in Slash Commands
If you need a command that executes hard‑coded logic rather than a recipe, modify the CLI parser directly:
- Open
crates/goose-cli/src/session/input.rs. - Add a new match arm in
handle_slash_command:
"/mybuiltin" => {
// Perform custom logic here
Some(InputResult::Message("Running builtin".to_string()))
}
- Add a corresponding variant to the
InputResultenum if you require a new return type.
After recompiling the CLI (cargo build -p goose-cli), the command is recognized immediately without requiring a recipe mapping.
Key Source Files
crates/goose/src/slash_commands.rs: DefinesSlashCommandMapping,resolve_slash_command, and persistence logic.crates/goose-cli/src/session/input.rs: Implementshandle_slash_commandfor built‑in command parsing.crates/goose/src/agents/execute_commands.rs: Invokes the slash command resolver during agent execution.crates/goose-server/src/routes/recipe.rs: Provides the HTTP endpoint for registering slash commands.crates/goose-server/src/routes/config_management.rs: Retrieves registered slash commands for UI display.
Summary
- Built‑in commands are static match arms in
handle_slash_commandwithin the CLI crate. - Custom commands are dynamic mappings stored in the global config under the
slash_commandskey asSlashCommandMappingstructs. - Use
set_recipe_slash_commandprogrammatically, POST to/recipes/slash-commandvia the API, or edit~/.goose/config.jsonto register new commands. - Custom commands fallback through the CLI input handler and resolve later in the agent loop by loading and executing the associated YAML recipe.
- For hard‑coded behavior, extend
handle_slash_commanddirectly and recompile the CLI.
Frequently Asked Questions
Where are custom goose slash commands stored?
Custom slash commands persist in the global Goose configuration file (typically ~/.goose/config.json) under the slash_commands key. Each entry is a JSON object containing the command name and the absolute or relative path to the associated recipe file.
What is the difference between built‑in and custom slash commands in Goose?
Built‑in commands such as /exit and /prompt are hard‑coded in the handle_slash_command function within crates/goose-cli/src/session/input.rs and execute immediate logic. Custom commands are user‑defined mappings that associate a slash string with a YAML recipe file; when triggered, Goose loads the recipe and executes its steps through the agent loop.
How do I register a custom slash command via the Goose API?
Send a POST request to the /recipes/slash-command endpoint with a JSON body containing file_path and slash_command fields. For example: curl -X POST -H "Content-Type: application/json" -d '{"file_path":"recipes/task.yaml","slash_command":"/task"}' https://goose/api/recipes/slash-command.
Can I create a slash command that runs custom code instead of a recipe?
Yes, but this requires adding a new built‑in command. You must modify the handle_slash_command function in crates/goose-cli/src/session/input.rs to include a new match arm that returns the appropriate InputResult. After modifying the source, recompile the CLI with cargo build -p goose-cli to make the command available.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →