Custom Commands
Fellow supports custom commands that let you define your own tools for the AI to use.
These commands follow the same structure as built-ins but live in your own project directory — typically .fellow/commands
.
You can use custom commands to automate repetitive tasks, interact with your specific environment, or override built-in behavior.
Purpose and Structure
A custom command in Fellow consists of two parts:
- A Pydantic model that defines the expected input arguments (
CommandInput
) - A handler function that implements the logic of the command
These are combined using the Command
class to form a complete callable tool.
When a task is executed, Fellow validates the input against your schema and passes in a shared execution context.
Additionally, the docstring of the function is extracted at runtime and used as a description when the AI client registers available tools — so make sure it’s short, clear, and helpful.
Example
Let’s say you want to create a command that counts how many times a word appears in a file:
from fellow.commands.command import CommandContext, CommandInput
from pydantic import Field
class CountWordInput(CommandInput):
filepath: str = Field(..., description="Path to the file")
word: str = Field(..., description="The word to count")
def count_word(args: CountWordInput, context: CommandContext) -> str:
"""Counts how often a given word appears in a file."""
try:
with open(args.filepath, "r") as f:
content = f.read()
count = content.count(args.word)
return f"The word '{args.word}' appears {count} times."
except FileNotFoundError:
return f"[ERROR] File not found: {args.filepath}"
except Exception as e:
return f"[ERROR] Unexpected error: {e}"
- The
CommandInput
class defines which fields the AI must provide - The handler function performs the task and returns a string response
- The function’s docstring explains to the AI what the command does
- The pydantic descriptions are used to generate tool documentation for the AI client
How to Create a Custom Command
The easiest way to scaffold a command is with:
fellow init-command my_custom_command
This will generate a Python file at the first path listed in custom_commands_paths
in your config (typically .fellow/commands/
).
It creates a boilerplate file that includes:
- a properly named
CommandInput
class - a handler function
- docstrings and descriptions
File Location and Config Registration
All custom commands must be located in a folder listed in your config under custom_commands_paths
.
For example, in your YAML config:
custom_commands_paths:
- ".fellow/commands"
You must also register your command explicitly in the commands
list:
commands:
- "create_file"
- "count_word"
...
If you forget to register the command, it won’t be available to the agent — even if it’s implemented correctly.
Overriding Built-in Commands
You can override any built-in command by using the same command name in your custom implementation.
For example, if you create a custom edit_file
in .fellow/commands/edit_file.py
and register it in your config, Fellow will use your version instead of the built-in one.
This makes it easy to tweak behavior without modifying the core codebase.
The run
Method and CommandContext
All commands are run through the same internal interface:
- The AI calls a command with arguments (as a JSON string)
- Fellow parses those into your
CommandInput
- The function receives two arguments:
args
— validated instance of yourCommandInput
context
— a dictionary containing:ai_client
: the active clientconfig
: the loaded config object
This context allows commands to:
- Log output
- Access config settings
- Call other AI tools if needed
You don’t need to worry about calling the run
method directly — Fellow handles it when executing the reasoning loop.
Example Use Case
Suppose you want to create a command that commits and pushes changes to Git:
from fellow.commands.command import CommandContext, CommandInput
from pydantic import Field
import subprocess
class GitPushInput(CommandInput):
message: str = Field(..., description="The commit message")
def git_push(args: GitPushInput, context: CommandContext) -> str:
try:
subprocess.run(["git", "add", "."], check=True)
subprocess.run(["git", "commit", "-m", args.message], check=True)
subprocess.run(["git", "push"], check=True)
return "[OK] Changes pushed to Git."
except Exception as e:
return f"[ERROR] Git push failed: {e}"
Register it in your config:
commands:
- git_push
Now the assistant can use this command to commit code autonomously.