Why this matters
Prompt commands are commands that author the next model turn. They do not do all the work themselves. Instead, they prepare the instruction the model will read next, along with the bounds that limit what that follow-up turn can do.
They still live in the commands layer because the slash command decides the prompt, the tool bounds, and the command metadata before the model does the follow-up work. That keeps discovery, permissions, and prompt shape in one place.
Three representative cases
/commit is the rich prompt case. It gathers repository context, limits the
follow-up turn to git-safe tools, and builds the prompt that will guide the
commit work.
The three representative cases are /commit, /statusline, and
createMovedToPluginCommand().
const ALLOWED_TOOLS = [
'Bash(git add:*)',
'Bash(git status:*)',
'Bash(git commit:*)',
]
function getPromptContent(): string {
const { commit: commitAttribution } = getAttributionTexts()
let prefix = ''
if (process.env.USER_TYPE === 'ant' && isUndercover()) {
prefix = getUndercoverInstructions() + '\n'
}
return `${prefix}## Context
- Current git status: !\`git status\`
- Current git diff (staged and unstaged changes): !\`git diff HEAD\`
- Current branch: !\`git branch --show-current\`
- Recent commits: !\`git log --oneline -10\`
## Git Safety Protocol
- NEVER update the git config
- NEVER skip hooks (--no-verify, --no-gpg-sign, etc) unless the user explicitly requests it
- CRITICAL: ALWAYS create NEW commits. NEVER use git commit --amend, unless the user explicitly requests it
- Do not commit files that likely contain secrets (.env, credentials.json, etc). Warn the user if they specifically request to commit those files
- If there are no changes to commit (i.e., no untracked files and no modifications), do not create an empty commit
- Never use git commands with the -i flag (like git rebase -i or git add -i) since they require interactive input which is not supported
## Your task
Based on the above changes, create a single git commit:
1. Analyze all staged changes and draft a commit message:
- Look at the recent commits above to follow this repository's commit message style
- Summarize the nature of the changes (new feature, enhancement, bug fix, refactoring, test, docs, etc.)
- Ensure the message accurately reflects the changes and their purpose (i.e. "add" means a wholly new feature, "update" means an enhancement to an existing feature, "fix" means a bug fix, etc.)
- Draft a concise (1-2 sentences) commit message that focuses on the "why" rather than the "what"
2. Stage relevant files and create the commit using HEREDOC syntax:
\`\`\`
git commit -m "$(cat <<'EOF'
Commit message here.${commitAttribution ? `\n\n${commitAttribution}` : ''}
EOF
)"
\`\`\`
You have the capability to call multiple tools in a single response. Stage and create the commit using a single message. Do not use any other tools or do anything else. Do not send any other text or messages besides these tool calls.`
}
const command = {
type: 'prompt',
name: 'commit',
description: 'Create a git commit',
allowedTools: ALLOWED_TOOLS,
contentLength: 0, // Dynamic content
progressMessage: 'creating commit',
source: 'builtin',
async getPromptForCommand(_args, context) {
const promptContent = getPromptContent()
The important part is the shape: allowedTools narrows the next turn, the
prompt content describes the job, and the command metadata says this is still a
built-in command even though the model will carry out the follow-up work.
/statusline is the short prompt case. It takes the user’s text, falls back to
a shell-PS1-based default, and then asks for a dedicated agent tool setup. The
allowed tool list, prompt text, and source metadata all sit beside each other in
the command object.
const statusline = {
type: 'prompt',
description: "Set up Claude Code's status line UI",
contentLength: 0,
// Dynamic content
aliases: [],
name: 'statusline',
progressMessage: 'setting up statusLine',
allowedTools: [AGENT_TOOL_NAME, 'Read(~/**)', 'Edit(~/.claude/settings.json)'],
source: 'builtin',
disableNonInteractive: true,
async getPromptForCommand(args): Promise<ContentBlockParam[]> {
const prompt = args.trim() || 'Configure my statusLine from my shell PS1 configuration';
return [{
type: 'text',
text: `Create an ${AGENT_TOOL_NAME} with subagent_type "statusline-setup" and the prompt "${prompt}"`
}];
}
} satisfies Command;
This is still a prompt command because the command is authoring the next model turn. The command does not itself create the status line. It writes the request that sends the model toward that work.
That is the contrast: /commit and /statusline carry explicit tool bounds,
while createMovedToPluginCommand() is a factory that returns prompt
commands without exposing an allowedTools list inside the factory itself.
createMovedToPluginCommand() is the factory case. It generates prompt-style
commands from one shared helper, then switches behavior when the command has
been moved into a plugin.
export function createMovedToPluginCommand({
name,
description,
progressMessage,
pluginName,
pluginCommand,
getPromptWhileMarketplaceIsPrivate,
}: Options): Command {
return {
type: 'prompt',
name,
description,
progressMessage,
contentLength: 0, // Dynamic content
userFacingName() {
return name
},
source: 'builtin',
async getPromptForCommand(
args: string,
context: ToolUseContext,
): Promise<ContentBlockParam[]> {
if (process.env.USER_TYPE === 'ant') {
return [
{
type: 'text',
text: `This command has been moved to a plugin. Tell the user:
1. To install the plugin, run:
claude plugin install ${pluginName}@claude-code-marketplace
2. After installation, use /${pluginName}:${pluginCommand} to run this command
3. For more information, see: https://github.com/anthropics/claude-code-marketplace/blob/main/${pluginName}/README.md
Do not attempt to run the command. Simply inform the user about the plugin installation.`,
},
]
}
How the pieces fit
type: 'prompt' tells the command system that the command authors prompt text
instead of running a local tool result.
allowedTools is the tool budget for the follow-up turn. It keeps the next
model step inside the command’s intended bounds.
getPromptForCommand() is where the prompt text gets assembled, and
source: 'builtin' is the metadata that says where this command came from.
Those two pieces answer different questions, so the runtime keeps them
separate on purpose.