Custom Agents, Commands & Tools

Define reusable agents, slash commands, and Python tools from files in .praisonai/ — no packaging required

python
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/.

Diagram

~/.praisonai/

Discovery

./.praisonai/

./.claude/

./.agents/

Custom agents

Custom commands

Custom tools
.praisonai only

praisonai run –agent

praisonai run –command

praisonai run auto-loaded

How It Works

Diagram
Custom Agents & CommandsAgentUserCustom Agents & CommandsAgentUserRequestProcess requestResult Agent–>>User: Response

Quick Start

bash
praisonai init
  1. 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.
  2. Run the agent
    bash
    praisonai run --agent researcher "What's new in WebAssembly 3.0?"
  3. Create a command file
    markdown
    <!-- .praisonai/commands/summarise.md -->
    ---
    description: Summarise text
    ---
    
    Summarise the following in three bullet points:
    
    $ARGUMENTS
  4. 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.

LocationScopeAgentsCommandsTools
~/.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):

  1. Nearer directory beats a more distant ancestor — a nested package’s definition overrides one inherited from a parent repo.
  2. Within the same directory level, .praisonai/ beats .claude/ and .agents/ — project-native definitions win over imported ones.
  3. User-global (~/.praisonai/) is overridden by any project-level match.
Diagram

Ancestor levels (walk up to git root)

Nearest level (cwd)

./.claude/

Level merge
.praisonai wins

./.agents/

./.praisonai/

../.claude/

Level merge
.praisonai wins

../.agents/

../.praisonai/

~/.praisonai/

Merge
nearer wins

Final registry

Reusing existing .claude/ or .agents/ layouts

Already have agents defined for another tool? PraisonAI picks them up as-is:

text
my-project/
├── .claude/
│   ├── agents/reviewer.md      ← picked up
│   └── commands/deploy.md      ← picked up
└── .agents/
    └── agents/planner.md       ← picked up
bash
praisonai run --agent reviewer "Review the diff on this branch"
praisonai run --command deploy staging

No 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

FieldDescription
modelLLM model
toolsTool list
roleAgent role
goalAgent goal
instructionsSystem instructions
modeCoarse 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.
permissionPer-capability allow / deny / ask rules
Markdown bodyBecomes 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:

Diagram

📝 frontmatter tools:

🔀 union + dedup

–tools name-list

–toolset group-list

.praisonai/tools/*.py
PRAISONAI_ALLOW_LOCAL_TOOLS=true

🤖 agent tool list

Example:

bash
# 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.greet

Before 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:

markdown
---
name: reviewer
mode: read-only
---
You are a meticulous code reviewer…

For finer control, use the permission: block:

markdown
---
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

ValueEffect
read-only / planRead-only scoping — denies mutating tools
accept-editsAuto-accept file edits
bypassSkip permission checks
subagentDelegatability 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.

markdown
<!-- .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

PatternBehaviour
$ARGUMENTSReplaced with user input
$1, $2, …, $nPositional 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/fileInlines 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.

bash
# 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:

markdown
<!-- .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: $ARGUMENTS

Invocation:

bash
praisonai run --command migrate SearchBar React Vue
# → template resolves to:
#   Rewrite the `SearchBar` component from `React` to `Vue`.
#   Full request: SearchBar React Vue
CaseTemplateUser argsResult
Basic positionalDeploy $1 to $2web stagingDeploy web to staging
Quoted token stays one argSay $1 to $2"hello world" friendSay hello world to friend
Windows path preservedcd $1C:\Users\alicecd C:\Users\alice
Out-of-range → literalLegacy price $100x y zLegacy price $100
Both patternsFirst: $1 / All: $ARGUMENTSfoo bar bazFirst: foo / All: foo bar baz
Injected $1 in user text is not re-scannedEcho: $ARGUMENTS$1 literalEcho: $1 literal
Injected $(evil) is escapedCmd: $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.

FieldAliasesTypeDefaultDescription
description—strNoneShort one-line description shown in help / listings
allow_shell—boolfalsePer-command opt-in for live ` !cmd ` substitution
argument-hintargument_hintstrNoneHuman-facing hint like <pr-number> [reviewer], surfaced in help / previews of the command
model—strNonePreferred LLM model to use when this command runs the agent (overrides the caller’s default)
toolsallowed-tools, allowed_toolslist[str] or comma/space stringNoneAllowed-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.

markdown
<!-- .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.

$ARGUMENTS

The single-line form of tools is equivalent to the YAML list above:

yaml
tools: github, grep, read_file

Opt-in live shell substitution

` !cmd ` is disabled by default. Enable with any one of:

  1. PRAISONAI_ALLOW_SHELL=true environment variable
  2. commands.allow_shell: true in .praisonai/config.yaml
  3. Per-command frontmatter allow_shell: true
markdown
---
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.

python
# .praisonai/tools/greet.py
from praisonaiagents import tool

@tool
def hello(name: str) -> str:
    """Say hello to name."""
    return f"Hello, {name}!"
bash
export PRAISONAI_ALLOW_LOCAL_TOOLS=true
praisonai run "Say hello to Alex"
# → agent auto-discovers greet.hello and calls it

That is the whole feature. The rest of this section is detail.

How discovery works

LocationScopeOverrides on collision?
~/.praisonai/tools/*.pyUser-global (all your projects)—
./.praisonai/tools/*.pyProject (walks up to the git root)✅ wins
Diagram

Discovery order

~/.praisonai/tools/*.py

Safe loader

./.praisonai/tools/*.py

Merge by name
project wins

Tool registry
module.func

What gets loaded

RuleBehaviour
Preferred exportsFunctions decorated with @tool — private helpers live safely alongside as plain defs
Fallback exportsIf no @tool functions exist in a file, all top-level public callables are loaded
NamespaceEvery callable is exposed as <module_stem>.<function_name> (e.g. greet.hello)
Skipped filesAny file whose name starts with _ (e.g. _helpers.py) is ignored
Skipped membersAny callable whose name starts with _, and names imported from other modules
Safety gateRequires PRAISONAI_ALLOW_LOCAL_TOOLS=true or --allow-local-tools (same gate as explicit local --tools)

Precedence & opt-out

SituationResult
PRAISONAI_ALLOW_LOCAL_TOOLS=true, .praisonai/tools/ presentAuto-loaded — callables added to the run’s tool list
praisonai run --tools my_toolExplicit --tools wins; auto-loaded tools still merge in (de-duped by identity)
PRAISONAI_ALLOW_LOCAL_TOOLS unsetAuto-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/ directoryNo-op

A richer example — private helpers stay private

python
# .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}"
bash
export PRAISONAI_ALLOW_LOCAL_TOOLS=true
praisonai run "Quote espresso at 3.50"
# → calls pricing.quote; _cents is never exposed to the LLM

Scaffold 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.

bash
praisonai agent create code-reviewer -d "Reviews diffs and suggests improvements" -p read-only --yes
# writes: .praisonai/agents/code-reviewer.md

The 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

bash
praisonai init
# writes:
#   .praisonai/agents/assistant.md
#   .praisonai/commands/review.md
#   .praisonai/tools/example.py   ← new

The 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.

markdown
---
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

Diagram

Named persona + tools

Reusable prompt template

Python callable for agents to use

Project instructions file

Packaged capability bundle

What do you need?

Custom agent

Custom command

Custom tool

Rules / AGENTS.md

Skill

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:

text
> /mydeploy staging

This 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.

python
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/*.pyProject-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/*.mdRules, 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.

Diagram

No

Yes

📝 Edit file
.praisonai/commands/foo.md
AGENTS.md etc.

🔍 Fingerprint
stat / listdir

🔁 Changed?

✅ Keep warm agent

♻️ Rebuild agent
on next prompt

💬 Conversation
restored from
session store

Diagram
AgentFilesystempraisonai codeUserAgentFilesystempraisonai codeUserEdit .praisonai/commands/review.md/review PR 42stat definition roots (fingerprint)Fingerprint changed → rebuild agentResolve /review from new fileResponse using edited command

Try the author-and-try loop:

bash
praisonai code

Add a new command file while the session stays open:

markdown
---
description: Draft a daily standup update
argument-hint: <yesterday> <today>
---

Draft a one-paragraph standup covering yesterday ($1) and today ($2).

$ARGUMENTS

Save it as .praisonai/commands/standup.md, then type in the live session:

text
> /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/*.py imports from site-packages. Edit the top-level tool file instead.
  • CLI flags passed to praisonai code at startup (model, workspace, permissions).
  • praisonai run — a one-shot command that already re-discovers definitions on every call.

Python API

python
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