Custom Agents, Commands & Tools
Define reusable agents, slash commands, and Python tools from files in .praisonai/ — no packaging required
from praisonaiagents import Agent
agent = Agent(name="custom-cmd-agent", instructions="Handle custom /commands in chat.")
agent.start("Register /summarise as a custom command that summarises the chat.")Drop Markdown, YAML, or Python files into .praisonai/agents/, .praisonai/commands/, and .praisonai/tools/ to extend the CLI without writing packaging code. Existing .claude/agents/, .claude/commands/, .agents/agents/, and .agents/commands/ layouts from other agent tools are picked up too — no migration required.
The user runs praisonai run --agent researcher; discovery loads custom agents, slash commands, and project-local tools from .praisonai/.
How It Works
Quick Start
praisonai init- Create an agent file
markdown <!-- .praisonai/agents/researcher.md --> --- model: gpt-4o role: Research Specialist tools: - web_search --- You are an expert researcher. Provide concise, cited answers. - Run the agent
bash praisonai run --agent researcher "What's new in WebAssembly 3.0?" - Create a command file
markdown <!-- .praisonai/commands/summarise.md --> --- description: Summarise text --- Summarise the following in three bullet points: $ARGUMENTS - Run the command
bash praisonai run --command summarise "Long article text here..."
How discovery works
Agents and commands are discovered from .praisonai/ and the conventional ecosystem roots .claude/ and .agents/. Tools stay .praisonai-only because loading them executes user code.
| Location | Scope | Agents | Commands | Tools |
|---|---|---|---|---|
~/.praisonai/agents/ / commands/ / tools/ | User-global | ✅ | ✅ | ✅ |
./.praisonai/agents/ / commands/ / tools/ | Project (walks up to git root) | ✅ | ✅ | ✅ |
./.claude/agents/ / commands/ | Project (walks up) — imported ecosystem root | ✅ | ✅ | ❌ (never) |
./.agents/agents/ / commands/ | Project (walks up) — imported ecosystem root | ✅ | ✅ | ❌ (never) |
Precedence (later wins on name collision):
- Nearer directory beats a more distant ancestor — a nested package’s definition overrides one inherited from a parent repo.
- Within the same directory level,
.praisonai/beats.claude/and.agents/— project-native definitions win over imported ones. - User-global (
~/.praisonai/) is overridden by any project-level match.
Reusing existing .claude/ or .agents/ layouts
Already have agents defined for another tool? PraisonAI picks them up as-is:
my-project/
├── .claude/
│ ├── agents/reviewer.md ← picked up
│ └── commands/deploy.md ← picked up
└── .agents/
└── agents/planner.md ← picked uppraisonai run --agent reviewer "Review the diff on this branch"
praisonai run --command deploy stagingNo migration, no copying. If you later add .praisonai/agents/reviewer.md, that project-native file wins.
Agent definitions
Files: .praisonai/agents/.md|.yaml, .claude/agents/.md|.yaml, .agents/agents/.md|.yaml
| Field | Description |
|---|---|
model | LLM model |
tools | Tool list |
role | Agent role |
goal | Agent goal |
instructions | System instructions |
mode | Coarse permission shorthand (build, read-only, plan, review) or the subagent delegatability marker — makes this agent delegatable from another running agent; does not set permissions. See Named Agent Delegation. |
permission | Per-capability allow / deny / ask rules |
| Markdown body | Becomes system_prompt when no instructions field |
Tool composition on praisonai run --agent
When praisonai run --agent <name> runs, the agent’s tool list is built from these sources, in order, and dedup’d by callable identity:
Example:
# researcher.md frontmatter → tools: [web_search]
# .praisonai/tools/greet.py → greet.greet
praisonai run --agent researcher --tools github --allow-local-tools \
"Greet Ada, then research WebAssembly 3.0"
# → final tool list: web_search, github, greet.greetBefore this was fixed (#3047), only the frontmatter list was honoured on the --agent path.
Scoping permissions
Add mode: to a definition for instant read-only or review scoping:
---
name: reviewer
mode: read-only
---
You are a meticulous code reviewer…For finer control, use the permission: block:
---
name: git-assistant
permission:
bash:
"git *": ask
"*": deny
read: allow
---
You are a git-aware assistant.See Agent Presets & Modes for the full modes reference, permission syntax, and precedence rules.
Supported mode: values
| Value | Effect |
|---|---|
read-only / plan | Read-only scoping — denies mutating tools |
accept-edits | Auto-accept file edits |
bypass | Skip permission checks |
subagent | Delegatability marker — makes the agent delegatable from other agents at run time. Applies no permission changes. |
mode: subagent marks an agent as delegatable from other agents at run time (see Named Agent Delegation). It is a marker only — it does not apply any permission changes and is ignored when computing permissions. Every other mode: value still flows through the permission engine as before.
<!-- .praisonai/agents/researcher.md — delegatable teammate -->
---
model: gpt-4o-mini
role: Research Specialist
goal: Gather accurate, cited facts on a topic
mode: subagent
tools:
- web_search
---
You are a meticulous researcher. Provide concise, cited findings.Command templates
Files: .praisonai/commands/.md, .claude/commands/.md, .agents/commands/*.md
| Pattern | Behaviour |
|---|---|
$ARGUMENTS | Replaced with user input |
$1, $2, …, $n | Positional token from the user’s argument string (quote-aware; Windows-path-safe). Injected in the same pass as $ARGUMENTS so user input is never re-scanned. Out-of-range positions (e.g. $100) are preserved as literal text. |
@path/to/file | Inlines file contents |
` !cmd ` | Opt-in live shell substitution — runs cmd and inlines stdout |
$(shell cmd) | Escaped — not executed (safety) |
Positional arguments
Reference $1, $2, …, $n to pull individual tokens out of the user’s argument string — $ARGUMENTS still holds the whole string.
# Agent-facing: ask the agent to author the command, then call it
praisonai run --agent build "Create .praisonai/commands/migrate.md that rewrites a component from one framework to another using $1 $2 $3."Bare-minimum command file:
<!-- .praisonai/commands/migrate.md -->
---
description: Rewrite a component from one framework to another
argument-hint: <component> <from> <to>
model: gpt-4o
tools: read_file, write_file, grep
---
Rewrite the `$1` component from `$2` to `$3`.
Full request: $ARGUMENTSInvocation:
praisonai run --command migrate SearchBar React Vue
# → template resolves to:
# Rewrite the `SearchBar` component from `React` to `Vue`.
# Full request: SearchBar React Vue| Case | Template | User args | Result |
|---|---|---|---|
| Basic positional | Deploy $1 to $2 | web staging | Deploy web to staging |
| Quoted token stays one arg | Say $1 to $2 | "hello world" friend | Say hello world to friend |
| Windows path preserved | cd $1 | C:\Users\alice | cd C:\Users\alice |
| Out-of-range → literal | Legacy price $100 | x y z | Legacy price $100 |
| Both patterns | First: $1 / All: $ARGUMENTS | foo bar baz | First: foo / All: foo bar baz |
Injected $1 in user text is not re-scanned | Echo: $ARGUMENTS | $1 literal | Echo: $1 literal |
Injected $(evil) is escaped | Cmd: $1 | $(rm -rf /) | Cmd: \$(rm -rf /) |
Command frontmatter
Frontmatter fields tune how a command runs. Every field is optional — a command with only a description still works.
| Field | Aliases | Type | Default | Description |
|---|---|---|---|---|
description | — | str | None | Short one-line description shown in help / listings |
allow_shell | — | bool | false | Per-command opt-in for live ` !cmd ` substitution |
argument-hint | argument_hint | str | None | Human-facing hint like <pr-number> [reviewer], surfaced in help / previews of the command |
model | — | str | None | Preferred LLM model to use when this command runs the agent (overrides the caller’s default) |
tools | allowed-tools, allowed_tools | list[str] or comma/space string | None | Allowed-tools hint scoping which tools the agent should have available when the command runs. Accepts a YAML list or a free-form string ("read_file, write_file grep" → ["read_file", "write_file", "grep"]) |
tools wins when present; otherwise allowed-tools / allowed_tools supply the list.
<!-- .praisonai/commands/review-pr.md -->
---
description: Review a pull request against the current branch
argument-hint: <pr-number> [reviewer]
model: gpt-4o
allowed-tools:
- github
- grep
- read_file
---
Review PR #$1 for correctness, security, and style. Assign secondary review to $2.
$ARGUMENTSThe single-line form of tools is equivalent to the YAML list above:
tools: github, grep, read_fileOpt-in live shell substitution
` !cmd ` is disabled by default. Enable with any one of:
PRAISONAI_ALLOW_SHELL=trueenvironment variablecommands.allow_shell: truein.praisonai/config.yaml- Per-command frontmatter
allow_shell: true
---
name: review
allow_shell: true
---
Review this diff:
!`git diff --stat`Safety bounds: 30s timeout, 100KB max stdout, runs in the template’s working directory. Non-zero exit raises ShellSubstitutionError. ` !cmd inside $ARGUMENTS or @file` contents is never executed — only markers in the original template run.
Project-local tools
Drop any .py file into .praisonai/tools/ and every praisonai run — including run --agent <name> — in that project auto-loads its tools — no --tools flag, no packaging, no imports in your agent code.
Only .praisonai/tools/*.py is auto-loaded. .claude/tools/ and .agents/tools/ are intentionally ignored, because loading a tool executes Python — this keeps a drop-in ecosystem checkout from running arbitrary code.
# .praisonai/tools/greet.py
from praisonaiagents import tool
@tool
def hello(name: str) -> str:
"""Say hello to name."""
return f"Hello, {name}!"export PRAISONAI_ALLOW_LOCAL_TOOLS=true
praisonai run "Say hello to Alex"
# → agent auto-discovers greet.hello and calls itThat is the whole feature. The rest of this section is detail.
How discovery works
| Location | Scope | Overrides on collision? |
|---|---|---|
~/.praisonai/tools/*.py | User-global (all your projects) | — |
./.praisonai/tools/*.py | Project (walks up to the git root) | ✅ wins |
What gets loaded
| Rule | Behaviour |
|---|---|
| Preferred exports | Functions decorated with @tool — private helpers live safely alongside as plain defs |
| Fallback exports | If no @tool functions exist in a file, all top-level public callables are loaded |
| Namespace | Every callable is exposed as <module_stem>.<function_name> (e.g. greet.hello) |
| Skipped files | Any file whose name starts with _ (e.g. _helpers.py) is ignored |
| Skipped members | Any callable whose name starts with _, and names imported from other modules |
| Safety gate | Requires PRAISONAI_ALLOW_LOCAL_TOOLS=true or --allow-local-tools (same gate as explicit local --tools) |
Precedence & opt-out
| Situation | Result |
|---|---|
PRAISONAI_ALLOW_LOCAL_TOOLS=true, .praisonai/tools/ present | Auto-loaded — callables added to the run’s tool list |
praisonai run --tools my_tool | Explicit --tools wins; auto-loaded tools still merge in (de-duped by identity) |
PRAISONAI_ALLOW_LOCAL_TOOLS unset | Auto-load disabled — a one-line hint is printed naming the tool files found and the enable step (--allow-local-tools or PRAISONAI_ALLOW_LOCAL_TOOLS=true) |
No .praisonai/tools/ directory | No-op |
A richer example — private helpers stay private
# .praisonai/tools/pricing.py
from praisonaiagents import tool
def _cents(usd: float) -> int: # skipped — no @tool
return int(round(usd * 100))
@tool
def quote(item: str, usd: float) -> str:
"""Return a formatted price quote for item at usd dollars."""
return f"{item}: ${_cents(usd) / 100:.2f}"export PRAISONAI_ALLOW_LOCAL_TOOLS=true
praisonai run "Quote espresso at 3.50"
# → calls pricing.quote; _cents is never exposed to the LLMScaffold a single agent with praisonai agent create
Turn a one-line description into a permission-scoped agent file in one command — no editor round-trip.
praisonai agent create code-reviewer -d "Reviews diffs and suggests improvements" -p read-only --yes
# writes: .praisonai/agents/code-reviewer.mdThe permission preset (read-only / review / full) maps to the same mode: shorthand documented in Scoping permissions above — no parallel grammar. When a provider credential is available, the system prompt is drafted via the LLM; otherwise the CLI writes an editable stub so file creation never blocks.
Use this when you want one focused agent; use praisonai init when you also want starter commands and a tools scaffold next to it.
See praisonai agent create for the full flag reference.
Scaffold with praisonai init
praisonai init
# writes:
# .praisonai/agents/assistant.md
# .praisonai/commands/review.md
# .praisonai/tools/example.py ← newThe scaffolded example.py is a commented @tool stub — uncomment or replace it in place. For a single-agent scaffold, see praisonai agent create.
Making an agent delegatable
Mark an agent with mode: subagent so a running primary agent can hand it sub-tasks by name.
---
name: researcher
model: gpt-4o
mode: subagent
---
You are an expert researcher.Now a running primary agent (praisonai run --agent lead) can call spawn_subagent(agent_name="researcher", …) and this agent runs the sub-task under its own model/tools/permissions. See Named Agent Delegation.
Agent vs command vs skill vs rule
Slash commands
Custom commands auto-register in interactive mode as CommandKind.CUSTOM. Disable with SlashCommandHandler(discover_custom=False).
Custom commands appear automatically in Telegram / Discord / autocomplete when your bot restarts, filtered by the same CommandAccessPolicy that gates execution. See Native / Autocomplete.
Inside praisonai code too
The same .praisonai/commands/*.md files work as /name inside praisonai code, the REPL, and the async TUI. A unified CommandRegistry aggregates built-ins, your custom commands, skills, MCP prompts, and pip-installed praisonai.commands packs into one namespace:
> /mydeploy stagingThis runs the exact interpolated template that praisonai run --command mydeploy staging would — byte-for-byte parity between interactive and CLI. See Slash Commands → Unified Command Registry.
Live edits during interactive sessions
Edits to a custom agent, command, skill, tool, or rules file take effect on the next prompt — no need to restart the session or the warm-runtime daemon.
from praisonaiagents import Agent
agent = Agent(
name="dev",
instructions="You edit files and iterate quickly.",
)
# Running `praisonai code` on top of this agent: edit .praisonai/commands/foo.md,
# then type /foo on the next prompt — no restart needed.| You edit… | Where it applies | |
|---|---|---|
.praisonai/commands/*.md (or .claude/commands/, .agents/commands/) | Interactive /<name> and praisonai daemon chats | |
| `.praisonai/agents/*.md\ | *.yaml (or .claude/agents/, .agents/agents/`) | Agent selection on the next prompt |
.praisonai/tools/*.py | Project-local tools, on the next prompt (still requires PRAISONAI_ALLOW_LOCAL_TOOLS=true) | |
.praisonai/skills/, .claude/skills/, ~/.praisonai/skills/, $PRAISONAI_HOME/skills/ | Skill registry, including SKILL.md body edits | |
.praisonai/rules/*.md | Rules, on the next prompt | |
CLAUDE.md, AGENTS.md, PRAISON.md, GEMINI.md, .cursorrules, .windsurfrules (and CLAUDE.local.md / PRAISON.local.md) in the current folder, any parent folder, or ~/ | Instruction context, on the next prompt |
The same agents/, commands/, rules/, skills/ and tools/ folders under .cursor/ and .windsurf/ are watched too.
Try the author-and-try loop:
praisonai codeAdd a new command file while the session stays open:
---
description: Draft a daily standup update
argument-hint: <yesterday> <today>
---
Draft a one-paragraph standup covering yesterday ($1) and today ($2).
$ARGUMENTSSave it as .praisonai/commands/standup.md, then type in the live session:
> /standup "shipped review bot" "ship hot-reload docs"Warm runtime (praisonai daemon / attach): both anonymous and --session <id> agents are dropped when a definition changes, and the next run rebuilds them with history rehydrated from the session store.
Not reloaded:
- Python modules that
.praisonai/tools/*.pyimports from site-packages. Edit the top-level tool file instead. - CLI flags passed to
praisonai codeat startup (model, workspace, permissions). praisonai run— a one-shot command that already re-discovers definitions on every call.
Python API
from praisonai_code.cli.features.custom_definitions import (
load_agent_from_name,
interpolate_command_template,
discover_project_tools, # NEW — returns loaded tool callables
)
config = load_agent_from_name("researcher")
prompt = interpolate_command_template("summarise", "Long text...")
tools = discover_project_tools() # [<greet.hello>, <pricing.quote>, ...]discover_project_tools() returns a list of tool callables (empty unless PRAISONAI_ALLOW_LOCAL_TOOLS=true). For metadata, CustomDefinitionsDiscovery().list_tools() returns CustomTool(name, path, callable, source) records where name is the namespaced <module>.<func>.
Best Practices
Use project files for team sharing
Commit .praisonai/agents/, .praisonai/commands/, and .praisonai/tools/ to git. Every teammate and CI run picks up the same tool set on the next praisonai run — no pip install and no --tools flag.
Decorate public tools, keep helpers private
.praisonai/tools/_helpers.py is skipped entirely. Inside a loaded file, functions without @tool are only fallback-loaded when the file has no @tool at all — so decorating your public entry points keeps private helpers off the LLM’s tool list.
Keep user-global files personal
Use ~/.praisonai/ for personal shortcuts that should not override team agents.
Keep project-native definitions in .praisonai/
Keep team-owned, project-native definitions in .praisonai/ — they win on name collision and are the single source PraisonAI writes to (e.g. praisonai init, praisonai agent create). Point PraisonAI at your existing .claude/agents/ or .agents/commands/ when you want to reuse assets that already ship in your repo for another agent tool, without duplicating them.
Never rely on unguarded shell substitution
` !cmd requires an explicit opt-in gate (allow_shell: true, config, or PRAISONAI_ALLOW_SHELL). $(…) is always escaped — use !cmd only when you need live output like git diff`.
Prefer $1..$n when the order matters
Positional args make the interpolation explicit and let you write help hints (argument-hint: <from> <to>) that survive editor tooltips. Use $ARGUMENTS only when the command should receive the raw request verbatim.
Set argument-hint for every non-trivial command
The hint shows up when the command is listed in praisonai command list / autocompleted in the interactive REPL and in bot slash-command menus. It’s the fastest way to teach yourself and your team what an argument looks like without opening the file.
Use tools: to sandbox the command
When set, the command runs with only those tools available even if the caller’s default agent has more. Pair with a read-only model: for review/inspection commands.
Start with praisonai init
Run praisonai init to scaffold .praisonai/agents/, .praisonai/commands/, and .praisonai/tools/ before hand-writing files.
Related
–agent and –command flags
Agent CLIList and inspect custom agents
Command CLIList and preview commands
Slash CommandsInteractive custom commands
Init CLIScaffold .praisonai/ in one command
ToolsThe @tool decorator and building custom tools
Project-Local ToolsAuto-discovered .praisonai/tools/*.py on run and run –agent
Agent Presets & ModesBuilt-in presets and per-agent permission scoping
Named Agent DelegationDelegate named sub-tasks to your agents with mode: subagent