Self-Improving Skills

Let agents propose, create, edit, and patch their own skills — with a human approval gate by default; archive, restore, and roll back at runtime

Self-improving skills enable agents to create, edit, and manage their own capabilities dynamically — with mutations staged for human approval before they touch disk.

python
from praisonaiagents import Agent

agent = Agent(
    name="Skill Builder",
    instructions="When users teach you something, save it as a skill for next time.",
    tools=["skill_manage", "skills_list", "skill_view"],
)
agent.start("Create a skill called 'weekly-summary' that summarises the week's work.")

The user teaches the agent; skill changes stay pending until a human approves them.

Diagram

Direct Write (propose=False)

🤖 Agent

💾 SKILL.md

Approval Gate (default)

approve

reject

👤 User Request

🤖 Agent

skill_manage

💾 Pending Store

👤 Human

✅ Live Skill

🗑️ Discarded

Quick Start

  1. Agent proposes a skill (default behaviour)
    python
    from praisonaiagents import Agent
    
    agent = Agent(
        name="Skill Builder",
        instructions="When users teach you something, save it as a skill for next time.",
        tools=["skill_manage", "skills_list", "skill_view"]
    )
    
    agent.start("Create a skill called 'weekly-summary' that summarises the week's work.")
    # Agent calls skill_manage(action="create", name="weekly-summary", content="...")
    # Tool returns: {"status": "pending", "id": "skl-a1b2c3d4"}
    # Agent replies: "Proposed — pending approval skl-a1b2c3d4"
  2. Human reviews and approves
    python
    from praisonaiagents import SkillManager
    
    mgr = SkillManager()
    
    # See what's waiting
    mgr.list_pending()
    # [{'id': 'skl-a1b2c3d4', 'action': 'create', 'name': 'weekly-summary', 'status': 'pending', 'created_at': '...'}]
    
    # Apply the mutation
    mgr.approve("skl-a1b2c3d4")
    # {"success": True, "skill": "weekly-summary"}
    
    # — or discard it —
    mgr.reject("skl-a1b2c3d4")
  3. Direct write (trusted/local contexts only)
    python
    from praisonaiagents import Agent
    
    agent = Agent(
        name="Skill Builder",
        instructions="Save skills immediately when users teach you something.",
        tools=["skill_manage", "skills_list", "skill_view"]
    )
    
    # Bypass the approval gate — writes straight to disk
    agent.start("Create a skill called 'csv-analysis' … [propose=False]")
  4. Archive and recover skills
    python
    from praisonaiagents import SkillManager
    
    mgr = SkillManager()
    mgr.discover()
    
    mgr.delete_skill("weekly-summary")          # archives by default (recoverable)
    mgr.restore_skill("weekly-summary")
    
    mgr.archive_skill("csv-analysis")
    archived = mgr.list_archived_skills()       # ["csv-analysis"]
    mgr.restore_skill("csv-analysis")

Approval Gate (safe-by-default)

Every mutation is staged by default so a human can review it before it becomes a live skill. The agent sees a pending response; you approve or reject via the SkillManager API or the skill_manage tool.

Diagram
👤 Human💾 Pending Store📋 SkillManager🤖 Agent👤 User👤 Human💾 Pending Store📋 SkillManager🤖 Agent👤 User“Create a weekly-summary skill”skill_manage(action=”create”, name=”weekly-summary”, …)stage proposal (audit: “proposed”){“status”: “pending”, “id”: “skl-a1b2c3d4”}“Proposed — pending approval skl-a1b2c3d4”skill_manage(action=”approve”, name=”skl-a1b2c3d4″)consume proposal (audit: “approved”)✅ Skill created

When to use propose=False

Diagram

Gateway / bot / unattended

Local dev / trusted CI

Mixed — set per-call

Where is the agent running?

propose=True
(default, safe)

propose=False
or SKILL_WRITE_APPROVAL=0

Pass propose= per call


How It Works

The skill management system provides six mutation actions, three approval actions, and four lifecycle actions:

ActionPurposeStaged by default?
createCreate new skills✅ Yes
editReplace skill content✅ Yes
patchTargeted find/replace✅ Yes
deleteArchive by default (recoverable)✅ Yes
write_fileAdd skill resources✅ Yes
remove_fileDelete skill files✅ Yes
archiveMove skill to archive store✅ Yes
restoreReturn archived skill to active✅ Yes
list_archivedList archived skills— (read-only)
rollbackUndo last edit/patch✅ Yes
pendingList staged mutations— (read-only)
approveApply a staged mutation— (approval action)
rejectDiscard a staged mutation— (approval action)

Configuration

Constructor & environment

OptionTypeDefaultDescription
SkillManager(write_approval=...)`bool \None`None (env-resolved)Per-instance override for staging policy
SKILL_WRITE_APPROVAL env varstr1 (true)Set to 0/false/off/no/disabled to disable staging by default
SKILL_MAX_PENDING env varint100Maximum pending proposals before new ones are rejected
propose (per call)`bool \None`None (inherits)True → stage; False → write directly; None → use manager default

Precedence: explicit propose= arg > SkillManager(write_approval=...) > SKILL_WRITE_APPROVAL env var > default True

python
import os
os.environ["SKILL_WRITE_APPROVAL"] = "0"   # disable staging globally

from praisonaiagents import SkillManager

# Per-instance override
mgr = SkillManager(write_approval=True)     # always stage, regardless of env

# Per-call override
mgr.create_skill("my-skill", "# content", propose=False)  # write directly this once

Skill management actions

ActionRequired ArgsOptional ArgsWhat it does
createname, contentcategory, proposeCreate a new skill (staged by default)
editname, contentproposeReplace an existing skill’s SKILL.md body
patchname, old_string, new_stringfile_path, replace_all, proposeFuzzy find-and-replace within a skill file
deletenamehard, proposeArchives by default; pass hard=True for permanent removal
write_filename, file_path, file_contentproposeAdd/overwrite a file inside the skill
remove_filename, file_pathproposeDelete a file from within the skill
archivenameproposeMove the skill to the recoverable archive store
restorenameskill_dir, proposeBring an archived skill back into active use
list_archived——List names of archived skills
rollbacknameproposeUndo the most recent edit/patch (single-step)
pending—name (filter)Returns list of staged mutations
approvename (id or skill name)—Apply the staged mutation
rejectname (id or skill name)—Discard the staged mutation

Python API Reference

Response shapes

Staged (default) — propose=True:

python
{
    "success": True,
    "status": "pending",
    "id": "skl-a1b2c3d4",   # short id for approve/reject
    "action": "create",
    "skill": "weekly-summary",
}

Direct write — propose=False:

python
{"success": True, "skill": "weekly-summary", "path": "/path/to/skill"}

Failure (either mode):

python
{"success": False, "error": "Error message"}

Mutation methods

python
from praisonaiagents import SkillManager

mgr = SkillManager()
mgr.discover()

# --- CREATE ---
# Default (staged):
mgr.create_skill("weekly-summary", "# Weekly Summary\nSteps...", category="reporting")
# → {"success": True, "status": "pending", "id": "skl-a1b2c3d4", "action": "create", "skill": "weekly-summary"}

# Direct write (trusted contexts only):
mgr.create_skill("weekly-summary", "# Weekly Summary\nSteps...", category="reporting", propose=False)
# → {"success": True, "skill": "weekly-summary", "path": "/path/to/skill"}

# --- EDIT ---
mgr.edit_skill("weekly-summary", "# Weekly Summary v2\n...")
# → {"success": True, "status": "pending", "id": "skl-b2c3d4e5", "action": "edit", "skill": "weekly-summary"}

mgr.edit_skill("weekly-summary", "# Weekly Summary v2\n...", propose=False)
# → {"success": True, "skill": "weekly-summary"}

# --- PATCH ---
mgr.patch_skill("weekly-summary", old_string="v2", new_string="v3")
# → {"success": True, "status": "pending", "id": "skl-c3d4e5f6", "action": "patch", "skill": "weekly-summary"}

mgr.patch_skill("weekly-summary", old_string="v2", new_string="v3", propose=False)
# → {"success": True, "skill": "weekly-summary", "replacements": 1}

# --- DELETE ---
mgr.delete_skill("weekly-summary")
# → {"success": True, "status": "pending", "id": "skl-d4e5f6g7", "action": "delete", "skill": "weekly-summary"}

mgr.delete_skill("weekly-summary", propose=False)
# → {"success": True, "skill": "weekly-summary", "path": "/path/to/skill"}

# --- WRITE_FILE ---
mgr.write_skill_file("weekly-summary", "scripts/report.py", "print('Weekly report')")
# → {"success": True, "status": "pending", "id": "skl-e5f6g7h8", "action": "write_file", "skill": "weekly-summary"}

mgr.write_skill_file("weekly-summary", "scripts/report.py", "print('Weekly report')", propose=False)
# → {"success": True, "skill": "weekly-summary", "file": "scripts/report.py"}

# --- REMOVE_FILE ---
mgr.remove_skill_file("weekly-summary", "scripts/report.py")
# → {"success": True, "status": "pending", "id": "skl-f6g7h8i9", "action": "remove_file", "skill": "weekly-summary"}

mgr.remove_skill_file("weekly-summary", "scripts/report.py", propose=False)
# → {"success": True, "skill": "weekly-summary", "file": "scripts/report.py"}

# --- ARCHIVE / RESTORE / ROLLBACK ---
mgr.archive_skill("weekly-summary", propose=False)
mgr.list_archived_skills()  # ["weekly-summary"]
mgr.restore_skill("weekly-summary", propose=False)
mgr.rollback_skill("weekly-summary", propose=False)  # undo last edit/patch via .skill.bak

Approval state machine

python
from praisonaiagents import SkillManager

mgr = SkillManager()

# List pending proposals
pending = mgr.list_pending()
# [{'id': 'skl-a1b2c3d4', 'action': 'create', 'name': 'weekly-summary', 'status': 'pending', 'created_at': '2026-06-24T11:00:00Z'}]

# Approve by ID (preferred) or skill name (most-recent-wins fallback)
mgr.approve("skl-a1b2c3d4")   # by ID
mgr.approve("weekly-summary")  # by skill name

# Reject by ID or skill name
mgr.reject("skl-a1b2c3d4")
mgr.reject("weekly-summary")

SkillMutatorProtocol aliases

SkillManager conforms to SkillMutatorProtocol. Short-form aliases are available alongside the _skill methods:

AliasLong form
mgr.create(name, content, category=None, propose=None)mgr.create_skill(...)
mgr.edit(name, content, propose=None)mgr.edit_skill(...)
mgr.patch(name, old, new, file_path=None, replace_all=False, propose=None)mgr.patch_skill(...)
mgr.delete(name, propose=None)mgr.delete_skill(...)
mgr.write_file(name, file_path, file_content, propose=None)mgr.write_skill_file(...)
mgr.remove_file(name, file_path, propose=None)mgr.remove_skill_file(...)
python
from praisonaiagents import SkillManager

mgr = SkillManager()

# Short-form aliases — same methods as create_skill, edit_skill, etc.
mgr.create("weekly-summary", "# Weekly Summary\nSteps...")
mgr.patch("weekly-summary", old_string="v1", new_string="v2", propose=False)

Pending Store & Audit Log

Both artefacts always live in the user-owned skills directory (~/.praisonai/skills/, honouring PRAISONAI_HOME), regardless of cwd — so a proposal staged from one project is discoverable when the approver runs from another:

PathFormatPurpose
~/.praisonai/skills/.pending_skills.jsonJSON map {id: record}Staged mutations awaiting approval
~/.praisonai/skills/.skill_audit.logJSON-linesAppend-only audit: proposed / approved / approval_failed / rejected events

Pending record shape:

json
{
  "id": "skl-a1b2c3d4",
  "action": "create",
  "name": "weekly-summary",
  "status": "pending",
  "created_at": "2026-06-24T11:00:00Z",
  "payload": {
    "content": "…",
    "category": "reporting",
    "base_dir": "/path/to/project-a/.praisonai/skills"
  }
}

Audit log line:

json
{"event": "approved", "timestamp": "2026-06-24T11:05:00Z", "id": "skl-a1b2c3d4", "action": "create", "name": "weekly-summary"}

Visibility rules

list_pending() only surfaces a pending directory when it carries a real SKILL.md. Stray temp directories or directories that contain nothing but a malformed .proposal.json are silently excluded so they cannot be advertised as approvable.

Pending dir contentsSurfaced by list_pending()?
SKILL.md + valid .proposal.json✅ Yes (action read from proposal)
SKILL.md, no proposal✅ Yes (treated as create)
SKILL.md + malformed .proposal.json✅ Yes (fallback to create)
Malformed .proposal.json, no SKILL.md❌ No
Empty / stray directory❌ No

Storage Location

SkillManager uses different rules for where it writes mutations versus where it discovers existing skills for read.

Write path (mutations)

create_skill, edit_skill, patch_skill, write_skill_file, and their aliases resolve their destination via _skills_base_dir():

  1. Project (preferred): ./.praisonai/skills/ — used when that directory already exists in the current working directory.
  2. User home (fallback): ~/.praisonai/skills/ — used otherwise. Honours PRAISONAI_HOME.

Writes never walk ancestor directories and never touch /etc/praison/skills — those apply to discovery only.

Approval writes back to the origin project

Staging a create with propose=True pins the resolved destination into the pending record (payload.base_dir). Approving that record later — even from a completely different working directory — writes the SKILL.md back to the project the proposal originated in.

Diagram
✅ project-A/.praisonai/skills/👤 Approver (any cwd)💾 ~/.praisonai/skills/.pending_skills.json🤖 Dev in project A✅ project-A/.praisonai/skills/👤 Approver (any cwd)💾 ~/.praisonai/skills/.pending_skills.json🤖 Dev in project Acreate_skill(propose=True)pins base_dir = project-A/.praisonai/skillsapprove(id)payload.base_dir = project-A/.praisonai/skillswrites SKILL.md at the pinned path

Discovery path (read)

Discovery (get_default_skill_dirs()) walks a broader list — unchanged, and applies to read only:

  1. Project: ./.praisonai/skills/ — and ./.claude/skills/ for compatibility
  2. Ancestor walk: any .praisonai/skills or .claude/skills in parent directories
  3. User: ~/.praisonai/skills/ (or $PRAISONAI_HOME/skills/)
  4. System (Unix): /etc/praison/skills/

Writes deliberately skip steps 2 and 4 so mutations can never land in an ancestor or admin-managed location.

Archive store: Archived skills are moved to <first_skill_dir>/../skills_archive/. If a name already exists in the archive, a UTC timestamp suffix is appended (e.g. weekly-summary.20240624153012).


Security Guards

GuardPurposeImplementation
Size LimitsPrevent resource exhaustion100KB SKILL.md, 1MB files
Name ValidationSecure identifiers[a-z0-9][a-z0-9._-]* pattern, 64 char limit
Path ValidationPrevent traversalSyntactic prefilter rejects absolute (/), home (~), null bytes, and any ../empty segment
Real ContainmentEnforce the boundaryCaller’s resolve().relative_to(skill_path.resolve())
Atomic WritesPrevent corruptionTemp file + rename operations
Allowed SubdirsRestrict file placementNested relative paths (references/, scripts/, assets/) allowed
Max PendingPrevent accumulationSKILL_MAX_PENDING=100 (101st returns error)

Nested file paths

Nested relative paths inside a skill directory now work in write_file and patch. The syntactic filter is a prefilter, not the security boundary — real containment is still enforced by the caller’s resolve().relative_to(skill_path.resolve()) check.

Still rejected (unchanged): absolute paths (/etc/...), user-home paths (~/...), null bytes (\0), and any .. segment — across both / and \\ separators.

Now allowed (new): nested relative paths such as references/palette.md, scripts/helper.py, and assets/img/logo.png.

write_file

python
from praisonaiagents.tools.skill_tools import skill_manage

# ✅ Allowed — nested relative path inside the skill
skill_manage(action="write_file", name="my-skill",
             file_path="references/palette.md", file_content="# Palette")

# ✅ Allowed — deeper nesting
skill_manage(action="write_file", name="my-skill",
             file_path="assets/img/logo.png", file_content="...")

# ❌ Rejected — traversal
skill_manage(action="write_file", name="my-skill",
             file_path="../etc/passwd", file_content="...")

# ❌ Rejected — absolute path
skill_manage(action="write_file", name="my-skill",
             file_path="/etc/passwd", file_content="...")

patch

python
from praisonaiagents.tools.skill_tools import skill_manage

# ✅ Allowed — patch a nested script
skill_manage(action="patch", name="my-skill",
             file_path="scripts/helper.py",
             old_string="v1", new_string="v2")

# ❌ Rejected — traversal
skill_manage(action="patch", name="my-skill",
             file_path="../../secret",
             old_string="a", new_string="b")

# ❌ Rejected — home path
skill_manage(action="patch", name="my-skill",
             file_path="~/notes.md",
             old_string="a", new_string="b")

Common Patterns

Complete approval flow

python
from praisonaiagents import Agent, SkillManager

# Agent proposes
agent = Agent(
    name="Learning Assistant",
    instructions="When users teach you something, save it as a skill.",
    tools=["skill_manage", "skills_list", "skill_view"]
)

agent.start("Here's how to analyze CSV files: load with pandas, check for nulls, then create summary stats")
# Agent calls: skill_manage(action="create", name="csv-analysis", content="...")
# Returns: {"status": "pending", "id": "skl-a1b2c3d4"}

# Human reviews and approves
mgr = SkillManager()
print(mgr.list_pending())
mgr.approve("skl-a1b2c3d4")
print("Skill is now live!")

Local dev — disable staging

python
import os
os.environ["SKILL_WRITE_APPROVAL"] = "0"

from praisonaiagents import Agent

agent = Agent(
    name="Dev Agent",
    instructions="Save skills immediately.",
    tools=["skill_manage", "skills_list", "skill_view"]
)

agent.start("Create a skill called 'csv-analysis' …")
# Writes to disk directly — no pending step

Skill evolution with approval

python
from praisonaiagents import SkillManager

mgr = SkillManager()
mgr.discover()

# Propose a patch — staged
result = mgr.patch_skill("csv-analysis", 
    old_string="2. Check for nulls",
    new_string="2. Handle missing headers\n3. Check for nulls")
print(result["id"])  # skl-…

# Apply it
mgr.approve(result["id"])

Safe edit with rollback

python
from praisonaiagents import SkillManager

mgr = SkillManager()
mgr.discover()

result = mgr.edit_skill("csv-analysis", "# CSV Analysis v2\nUpdated steps...", propose=False)
if result["success"]:
    rollback = mgr.rollback_skill("csv-analysis", propose=False)
    print("Reverted to previous version:", rollback)

Using the skill_manage tool actions directly

python
from praisonaiagents.tools.skill_tools import skill_manage
import json

# Propose creation
result = json.loads(skill_manage(action="create", name="my-skill", content="# My Skill"))
print(result["id"])  # skl-…

# List pending
pending = json.loads(skill_manage(action="pending", name=None))
print(pending["pending"])

# Approve
json.loads(skill_manage(action="approve", name="skl-a1b2c3d4"))

# Reject
json.loads(skill_manage(action="reject", name="skl-a1b2c3d4"))

Skill Bundles

Group related skills into named, reusable sets with praisonai skills bundle list and praisonai skills bundle show <name>. Select a whole bundle from an agent with skills=["@backend-dev"]. See Skill Bundles for the full guide.


Troubleshooting


Best Practices

Keep propose=True for unattended gateway/bot deployments

In any unattended context — Slack bots, webhooks, scheduled agents — leave propose=True (the default). This ensures no skill mutation reaches disk without a human seeing it first. Set SKILL_WRITE_APPROVAL=0 only on machines you control and trust.

Use SKILL_WRITE_APPROVAL=0 only in trusted local CI/dev

Direct writes are convenient for rapid iteration in local development or a controlled CI pipeline where you own the environment. Never disable staging for agents exposed to external users or production traffic.

Approve by ID, not by name

approve("skl-a1b2c3d4") is exact — it targets one specific proposal. approve("weekly-summary") uses a most-recent-wins fallback and can silently target the wrong proposal if multiple are pending for the same skill.

Security-First Design

All skill operations are workspace-contained and use atomic writes via temp files. Never bypass name validation or path checks. Skills inherit workspace security automatically.

Error Handling

All skill operations return detailed JSON results with success flags and error messages. Always check result["success"] before proceeding. On approval failure, the pending record is kept — check mgr.list_pending() and retry.

Don't pre-create empty pending directories

A pending directory is only approvable when it contains SKILL.md. If you stage skills outside the skill_manage tool (e.g. via a script that writes to <skills_dir>/pending/<name>/), write SKILL.md first — approval will fail until it is present, and list_pending() will not show the entry.


Concurrency

The pending-approval store is safe under concurrent proposals and approvals: _stage_pending(), approve(), and reject() serialise the load-mutate-save cycle behind a per-manager threading.Lock, so two agents proposing at the same time no longer clobber each other’s entries in .pending_skills.json. Audit-log writes happen outside the lock, so a slow disk on the audit sink can’t stall subsequent approvals.

Diagram
sequenceDiagram
    participant A1 as Agent A
    participant A2 as Agent B
    participant Mgr as SkillManager
    participant Store as .pending_skills.json

    par Concurrent proposals
        A1->>Mgr: skill_manage("edit", "planner")
    and
        A2->>Mgr: skill_manage("create", "summariser")
    end
    Note over Mgr: _pending_lock serialises load→mutate→save
    Mgr->>Store: write A's record
    Mgr->>Store: write B's record
    Note over Mgr,Store: both entries preserved

    classDef agent fill:#8B0000,stroke:#7C90A0,color:#fff
    classDef mgr fill:#189AB4,stroke:#7C90A0,color:#fff
    classDef store fill:#6366F1,stroke:#7C90A0,color:#fff
    class A1,A2 agent
    class Mgr mgr
    class Store store

Related