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.
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.
Quick Start
- 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" - 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") - 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]") - 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.
When to use propose=False
How It Works
The skill management system provides six mutation actions, three approval actions, and four lifecycle actions:
| Action | Purpose | Staged by default? |
|---|---|---|
| create | Create new skills | ✅ Yes |
| edit | Replace skill content | ✅ Yes |
| patch | Targeted find/replace | ✅ Yes |
| delete | Archive by default (recoverable) | ✅ Yes |
| write_file | Add skill resources | ✅ Yes |
| remove_file | Delete skill files | ✅ Yes |
| archive | Move skill to archive store | ✅ Yes |
| restore | Return archived skill to active | ✅ Yes |
| list_archived | List archived skills | — (read-only) |
| rollback | Undo last edit/patch | ✅ Yes |
| pending | List staged mutations | — (read-only) |
| approve | Apply a staged mutation | — (approval action) |
| reject | Discard a staged mutation | — (approval action) |
Configuration
Constructor & environment
| Option | Type | Default | Description | |
|---|---|---|---|---|
SkillManager(write_approval=...) | `bool \ | None` | None (env-resolved) | Per-instance override for staging policy |
SKILL_WRITE_APPROVAL env var | str | 1 (true) | Set to 0/false/off/no/disabled to disable staging by default | |
SKILL_MAX_PENDING env var | int | 100 | Maximum 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
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 onceSkill management actions
| Action | Required Args | Optional Args | What it does |
|---|---|---|---|
create | name, content | category, propose | Create a new skill (staged by default) |
edit | name, content | propose | Replace an existing skill’s SKILL.md body |
patch | name, old_string, new_string | file_path, replace_all, propose | Fuzzy find-and-replace within a skill file |
delete | name | hard, propose | Archives by default; pass hard=True for permanent removal |
write_file | name, file_path, file_content | propose | Add/overwrite a file inside the skill |
remove_file | name, file_path | propose | Delete a file from within the skill |
archive | name | propose | Move the skill to the recoverable archive store |
restore | name | skill_dir, propose | Bring an archived skill back into active use |
list_archived | — | — | List names of archived skills |
rollback | name | propose | Undo the most recent edit/patch (single-step) |
pending | — | name (filter) | Returns list of staged mutations |
approve | name (id or skill name) | — | Apply the staged mutation |
reject | name (id or skill name) | — | Discard the staged mutation |
Python API Reference
Response shapes
Staged (default) — propose=True:
{
"success": True,
"status": "pending",
"id": "skl-a1b2c3d4", # short id for approve/reject
"action": "create",
"skill": "weekly-summary",
}Direct write — propose=False:
{"success": True, "skill": "weekly-summary", "path": "/path/to/skill"}Failure (either mode):
{"success": False, "error": "Error message"}Mutation methods
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.bakApproval state machine
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:
| Alias | Long 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(...) |
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:
| Path | Format | Purpose |
|---|---|---|
~/.praisonai/skills/.pending_skills.json | JSON map {id: record} | Staged mutations awaiting approval |
~/.praisonai/skills/.skill_audit.log | JSON-lines | Append-only audit: proposed / approved / approval_failed / rejected events |
Pending record shape:
{
"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:
{"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 contents | Surfaced 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():
- Project (preferred):
./.praisonai/skills/— used when that directory already exists in the current working directory. - User home (fallback):
~/.praisonai/skills/— used otherwise. HonoursPRAISONAI_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.
Discovery path (read)
Discovery (get_default_skill_dirs()) walks a broader list — unchanged, and applies to read only:
- Project:
./.praisonai/skills/— and./.claude/skills/for compatibility - Ancestor walk: any
.praisonai/skillsor.claude/skillsin parent directories - User:
~/.praisonai/skills/(or$PRAISONAI_HOME/skills/) - 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
| Guard | Purpose | Implementation |
|---|---|---|
| Size Limits | Prevent resource exhaustion | 100KB SKILL.md, 1MB files |
| Name Validation | Secure identifiers | [a-z0-9][a-z0-9._-]* pattern, 64 char limit |
| Path Validation | Prevent traversal | Syntactic prefilter rejects absolute (/), home (~), null bytes, and any ../empty segment |
| Real Containment | Enforce the boundary | Caller’s resolve().relative_to(skill_path.resolve()) |
| Atomic Writes | Prevent corruption | Temp file + rename operations |
| Allowed Subdirs | Restrict file placement | Nested relative paths (references/, scripts/, assets/) allowed |
| Max Pending | Prevent accumulation | SKILL_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
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
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
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
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 stepSkill evolution with approval
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
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
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.
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 storeRelated
Provenance, telemetry, archive/restore, and rollback for agent-created skills
CardPick up newly installed / edited skills without restarting
Agent SkillsLoad and configure SKILL.md skills on agents
WorkspaceHow workspace containment secures skill operations
Skill Capability GatesCapability requirements and enforcement for skills
Self-Improving AgentsAuto-trigger skill_manage after each task with self_improve=True