diff --git a/strix/agents/factory.py b/strix/agents/factory.py
index be5075d..6eceef3 100644
--- a/strix/agents/factory.py
+++ b/strix/agents/factory.py
@@ -40,6 +40,7 @@ from strix.tools.agents_graph.tools import (
wait_for_message,
)
from strix.tools.finish.tool import finish_scan
+from strix.tools.load_skill.tool import load_skill
from strix.tools.notes.tools import (
create_note,
delete_note,
@@ -342,6 +343,8 @@ def _finish_tool_use_behavior(
_BASE_TOOLS: tuple[Tool, ...] = (
# Thinking + planning
think,
+ # On-demand skill reference (returns the skill markdown inline)
+ load_skill,
# Per-agent todos
create_todo,
list_todos,
diff --git a/strix/agents/prompt.py b/strix/agents/prompt.py
index bccfe78..5263e82 100644
--- a/strix/agents/prompt.py
+++ b/strix/agents/prompt.py
@@ -12,7 +12,7 @@ from typing import Any
from jinja2 import Environment, FileSystemLoader, select_autoescape
-from strix.skills import load_skills
+from strix.skills import get_available_skills, load_skills
from strix.utils.resource_paths import get_strix_resource_path
@@ -114,6 +114,7 @@ def render_system_prompt(
rendered = env.get_template("system_prompt.jinja").render(
loaded_skill_names=list(skill_content.keys()),
+ available_skills=get_available_skills(),
interactive=interactive,
system_prompt_context=system_prompt_context or {},
**skill_content,
diff --git a/strix/agents/prompts/system_prompt.jinja b/strix/agents/prompts/system_prompt.jinja
index 3fbfcc7..32738a7 100644
--- a/strix/agents/prompts/system_prompt.jinja
+++ b/strix/agents/prompts/system_prompt.jinja
@@ -437,3 +437,13 @@ Default user: pentester (sudo available)
{% endfor %}
{% endif %}
+
+{% if available_skills %}
+
+On-demand specialist skills. Spawn a specialist via `create_agent(skills=[...])`, or pull guidance inline for yourself via `load_skill(skills=[...])`. Anything wrapped in `` above is already loaded for you.
+
+{% for category, names in available_skills | dictsort -%}
+- {{ category }}: {{ names | join(', ') }}
+{% endfor -%}
+
+{% endif %}
diff --git a/strix/skills/__init__.py b/strix/skills/__init__.py
index 1eac077..d17c76b 100644
--- a/strix/skills/__init__.py
+++ b/strix/skills/__init__.py
@@ -1,5 +1,6 @@
import logging
import re
+from collections.abc import Iterator
from strix.utils.resource_paths import get_strix_resource_path
@@ -14,25 +15,31 @@ _FRONTMATTER_PATTERN = re.compile(r"^---\s*\n.*?\n---\s*\n", re.DOTALL)
_INTERNAL_SKILL_CATEGORIES: frozenset[str] = frozenset({"scan_modes", "coordination"})
-def get_all_skill_names() -> set[str]:
- """Return every user-selectable skill name (bare, no category prefix).
-
- Used by :func:`validate_requested_skills` so ``create_agent`` can
- reject typos / hallucinated names with a useful list, and by callers
- that need to enumerate the catalog.
- """
+def _iter_user_skill_files() -> Iterator[tuple[str, str]]:
+ """Yield ``(category_name, skill_name)`` for every user-selectable skill."""
skills_dir = get_strix_resource_path("skills")
if not skills_dir.exists():
- return set()
- names: set[str] = set()
- for category_dir in skills_dir.iterdir():
+ return
+ for category_dir in sorted(skills_dir.iterdir()):
if not category_dir.is_dir() or category_dir.name.startswith("__"):
continue
if category_dir.name in _INTERNAL_SKILL_CATEGORIES:
continue
- for file_path in category_dir.glob("*.md"):
- names.add(file_path.stem)
- return names
+ for file_path in sorted(category_dir.glob("*.md")):
+ yield category_dir.name, file_path.stem
+
+
+def get_all_skill_names() -> set[str]:
+ """Return every user-selectable skill name (bare, no category prefix)."""
+ return {name for _, name in _iter_user_skill_files()}
+
+
+def get_available_skills() -> dict[str, list[str]]:
+ """Return user-selectable skills grouped by category, alphabetised."""
+ grouped: dict[str, list[str]] = {}
+ for category, name in _iter_user_skill_files():
+ grouped.setdefault(category, []).append(name)
+ return grouped
def validate_requested_skills(skill_list: list[str], max_skills: int = 5) -> str | None:
diff --git a/strix/tools/load_skill/__init__.py b/strix/tools/load_skill/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/strix/tools/load_skill/tool.py b/strix/tools/load_skill/tool.py
new file mode 100644
index 0000000..3ef6f9e
--- /dev/null
+++ b/strix/tools/load_skill/tool.py
@@ -0,0 +1,36 @@
+"""``load_skill`` — fetch skill reference material into the conversation."""
+
+from __future__ import annotations
+
+from agents import RunContextWrapper, function_tool
+
+from strix.skills import load_skills, validate_requested_skills
+
+
+@function_tool(timeout=10)
+async def load_skill(ctx: RunContextWrapper, skills: list[str]) -> str:
+ """Return the markdown body of one or more skills as reference material.
+
+ Use this when you need exact syntax / workflow / payload guidance
+ right before acting on a technology that wasn't preloaded for your
+ agent. The skill content lands inline as a tool result — no
+ permanent prompt change, just in-conversation reference.
+
+ For permanent skill assignment, pass ``skills=[…]`` to
+ ``create_agent`` when spawning a specialist child instead.
+
+ Args:
+ skills: List of skill names (e.g. ``["xss", "sql_injection"]``).
+ Max 5. Names match the bare files under
+ ``strix/skills//.md``.
+ """
+ del ctx
+ requested = list(skills or [])
+ err = validate_requested_skills(requested)
+ if err:
+ return f"load_skill: {err}"
+ contents = load_skills(requested)
+ if not contents:
+ return "load_skill: no content loaded for requested skills."
+ sections = [f"## Skill: {name}\n\n{body}" for name, body in contents.items()]
+ return "\n\n---\n\n".join(sections)