Part 7: Implementing commands
Skills are loaded by the agent. Commands are invoked by the user. That is the main difference.
When the user types /kid, there is no inference step. The user is saying
which command to run. The system should load that command, render its prompt,
and give the rendered prompt to the agent.
Two implementation options
There are two practical ways to implement slash commands:
- Intercept the user input before the runner sees it.
- Expose command execution as a tool.
The first version is closer to the user-facing shape in coding agents. If the
input starts with /, your application loads the command, replaces the input
with the rendered prompt, and sends that prompt to the model.
The notebook follows the second version because it is easier to add without
changing ToyAIKit's runner. We create an execute_command tool and instruct the
agent to call it when the user asks for a slash command.
Command markdown format
Commands are markdown files with YAML frontmatter. Unlike skills, each command is usually one file, not a folder.
The /kid command has this shape:
---
description: The Claude Code Kid asks for random coding things to implement!
---
You are the Claude Code Kid - a wildly UNPREDICTABLE coding child!
Come up with something NEW and SURPRISING each time!
## YOUR MISSION:
Invent a COMPLETELY ORIGINAL coding request each time.
For the notebook demo, create a commands/ folder and download the two command
files from the opening demo:
mkdir commands
cd commands
wget https://raw.githubusercontent.com/alexeygrigorev/claude-code-kid-parent/refs/heads/main/.claude/commands/kid.md
wget https://raw.githubusercontent.com/alexeygrigorev/claude-code-kid-parent/refs/heads/main/.claude/commands/parent.md
The command name comes from the filename. kid.md becomes /kid, and
parent.md becomes /parent.
The Command data class
Create a small command object:
@dataclass
class Command:
name: str
description: str
template: str
The command body is called a template because command files can contain
placeholders such as $1, $2, and $ARGUMENTS. The notebook keeps template
processing simple. The prototype implements the substitutions.
The command loader
Load one command by name:
class CommandLoader:
def __init__(self, commands_dir: Path | str = None):
self.commands_dir = Path(commands_dir)
def load_command(self, name: str) -> Command:
command_file = self.commands_dir / f"{name}.md"
if not command_file.exists():
return None
parsed = frontmatter.load(command_file, encoding="utf-8")
metadata = dict(parsed.metadata)
return Command(
name=name,
description=metadata.get("description", ""),
template=parsed.content,
)
List all command files with *.md:
def list_commands(self) -> list[Command]:
"""List all available commands."""
commands = []
if not self.commands_dir.exists():
return commands
for md_file in sorted(self.commands_dir.glob("*.md")):
name = md_file.stem
command = self.load_command(name)
commands.append(command)
return commands
Create the loader and test it:
command_loader = CommandLoader(Path("commands/"))
command_loader.load_command("kid")
The result should include the description from frontmatter and the command body
as template.
The command tool
The notebook uses a placeholder processor that returns the template unchanged:
def process_template(template: str, arguments: str) -> str:
# process $1, $2, $3, $ARGUMENTS
return template
Wrap the loader as a tool:
class CommandsTool:
def __init__(self, command_loader: CommandLoader):
self.command_loader = command_loader
def execute_command(self, name: str, arguments: str = "") -> str:
"""Execute a command by name and return the rendered prompt.
If you see input starting with /command, use this tool to load and execute it.
If the command doesn't exist, let the user know.
Args:
name: The command name (without the / prefix).
arguments: Optional arguments to substitute into the template.
Returns:
The rendered command template as a string.
"""
if name.startswith("/"):
name = name.lstrip("/")
command = self.command_loader.load_command(name)
if not command:
return f"Command not found: /{name}"
return process_template(command.template, arguments)
The lstrip("/") is defensive. Smaller models sometimes include the slash even
when the tool docstring asks for the command name without it.
Create the tool and test it directly:
commands_tool = CommandsTool(command_loader=command_loader)
commands_tool.execute_command("kid")
Then expose it to the agent:
tools_obj.add_tools(commands_tool)
Now the registry contains the coding tools, the skill tool, and
execute_command.
Tell the agent not to confuse skills and commands
Update the injected prompt so the model knows the distinction:
SKILL_INJECTION_PROMPT = f"""
You have the following skills available which you can load with the skills tool:
{skill_loader.get_description()}
Don't confuse skills and commands:
- Skills are discovered automatically, without user explicitly asking for it
- Instructions to execute commands are given explicitly: "/test" -> "run the 'test' command"
When you see "/command", use the tools to execute the command "command"
""".strip()
Append it to the base prompt again:
AGENT_WITH_SKILLS_INSTRUCTIONS = (
AGENT_INSTRUCTIONS + "\n\n" + SKILL_INJECTION_PROMPT
)
This is wordier than the skill-only version because gpt-4o-mini benefits
from direct instructions. A stronger model may infer the pattern from tool
descriptions, but the workshop keeps it explicit.
Run /kid
Create a new runner with the updated tools and prompt:
runner = OpenAIResponsesRunner(
tools=tools_obj,
developer_prompt=AGENT_WITH_SKILLS_INSTRUCTIONS,
llm_client=llm_client,
chat_interface=chat_interface,
)
Run it:
result = runner.run()
Ask the agent to run the kid command:
/kid
The agent should call execute_command, load kid.md, and return a playful
app idea. The exact idea does not matter. What matters is that /kid becomes
the command template before the agent acts on it.
If you try a missing command such as /test without a test.md file, the tool
returns:
Command not found: /test
That is enough for the notebook version. The prototype adds a more realistic
command layer, argument substitution, and catches /command before it reaches
the model.
Continue with Part 8: The fuller prototype to connect the notebook code to the fuller implementation.