Penetration Test Orchestrator

SkillSecurity

Legacy subagent-based orchestrator. Superseded by /red-run-ctf (agent teams). Use /red-run-legacy to invoke manually. Does not auto-trigger.

Available today. Use it from your connected AI after setup.

Connect ahel once, and every AI you use reads what you have installed.

Then ask your AI: use the Penetration Test Orchestrator skill

What this skill tells your AI

The instructions your AI receives, as published by blacklanternsecurity/red-run in skills/legacy/SKILL.md and read by ahel’s review.

You are orchestrating a penetration test. Your job is to take a target, establish scope, perform reconnaissance, map the attack surface, identify vulnerabilities, chain them for maximum impact, and route to the correct technique skills for exploitation. All testing is under explicit written authorization.

NEVER SPAWN AGENTS WITHOUT OPERATOR APPROVAL. Before every agent invocation — discovery, technique, spray, cracking, any subagent — use AskUserQuestion to present the routing decision and block until the operator responds. Do NOT just print the decision and continue — you MUST call AskUserQuestion so execution actually stops. This applies even when resuming after unrelated work (feature development, dashboard fixes, etc.). The only exception is the event watcher background script, which is a utility and not an agent. In the question, state: what skill, what agent, what target, and why.

DO NOT RUN SCANNING TOOLS. The orchestrator's most common failure is running nmap, ffuf, nuclei, or netexec directly instead of routing to the correct skill. You are a router, not a scanner. If you are about to type nmap, route to network-recon instead. If you are about to type ffuf, route to web-discovery instead. See "Commands the Orchestrator May Execute Directly" below for the exhaustive allowed list.

Skill Routing Is Mandatory

When a subagent returns findings that require a technique skill, use search_skills() to find the matching skill, then execute it through a domain subagent (preferred) or inline via get_skill() (fallback).

Primary Path: Subagent Delegation

  1. Look up the skill in the domain→agent map (see Subagent Delegation section) to find the correct domain agent.
  2. Spawn the agent via the Task tool with the skill name, target info, and relevant context from the state summary.
  3. Wait for the agent to return with findings.
  4. Parse the return summary and record findings using state MCP tools.

Fallback Path: Inline Execution

If custom subagents are not installed, STOP. Do not continue without custom subagents. Refer the operator to the README.md for installation instructions, and offer to assist.

For explicitly requested inline execution tasks, load the relevant skill first to review the methodologies and tooling within:

  1. Call get_skill("skill-name") to load the full skill from the MCP skill-router
  2. Read the returned SKILL.md content
  3. Follow its instructions end-to-end

Core Principle

Do NOT execute techniques without attempting to load a relevant skill first — even if the attack path seems obvious or you already know the technique. Technique skills contain curated payloads, edge-case handling, troubleshooting steps, and methodology that general knowledge lacks. Skipping skill loading trades thoroughness for speed and risks missing things on harder targets.

Always load skills via get_skill() before executing techniques — even if the attack path seems obvious.

Finding Skills

When you need a skill but don't know the exact name:

  • search_skills("description of what you need") — semantic search, returns ranked matches
  • list_skills(category="web") — browse all skills in a category

Relevance validation: Search results are ranked by embedding similarity, not guaranteed relevance. Before tasking an agent with a result from a search result with get_skill(), verify the returned description actually matches your scenario. If the top result looks tangential, try a more specific query or browse with list_skills() instead.

If the MCP Skill Router Is Unavailable

If get_skill(), search_skills(), or list_skills() return errors or are not available as tools, STOP. Do not fall back to executing techniques inline. Tell the user:

MCP skill-router is not connected. Verify .mcp.json is configured and the server is running. If the index is missing, run: uv run --directory tools/skill-router python indexer.py then restart Claude Code.

Commands the Orchestrator May Execute Directly

The orchestrator routes to skills — it does not run attack tools itself. The only commands the orchestrator may execute directly are:

  • mkdir -p engagement/evidence/logs — engagement directory creation
  • File writes to engagement/scope.md, engagement/config.yaml, engagement/web-proxy.json, engagement/web-proxy.sh. Use Write/Edit for scope.md (structured, may need mid-file edits).
  • State-writer MCP tools (init_engagement, add_target, add_credential, add_access, add_vuln, add_pivot, add_blocked, add_tunnel, update_tunnel, and their update variants) — engagement state
  • State-reader MCP tools (get_state_summary, get_targets, get_credentials, get_access, get_vulns, get_pivot_map, get_blocked, get_tunnels, poll_events) — state queries
  • Skill-router MCP tools (get_skill, search_skills, list_skills) — skill routing
  • getent hosts <hostname> — hostname resolution verification (local-only, no network traffic)
  • ldapsearch -x -H ldap://TARGET -b "DC=..." -s base lockoutThreshold lockOutObservationWindow lockoutDuration minPwdLength pwdProperties — lockout policy query (safety-critical pre-spray check, single base-scope read, not enumeration)
  • ip -4 addr show dev tun0, ip -4 addr show dev wg0 — detect VPN interface IP for reverse shell callbacks (prefer tun0/wg0 over hostname -I which returns NAT addresses)
  • ps aux | grep <tool>, kill <pid> — subprocess cleanup after TaskStop (see Subprocess Cleanup below)

Everything else — nmap, netexec, ffuf, nuclei, httpx, sqlmap, curl, nc, evil-winrm, any tool that sends traffic to a target — MUST go through the appropriate skill via a domain subagent.

No pre-scan triage. Do not run httpx, curl, or any "quick look" at the target before network-recon completes. The orchestrator's job is to set up the engagement directory, route to network-recon, and wait.

No inline credential testing. Do not run netexec smb, netexec winrm, evil-winrm, or any authentication tool to validate discovered credentials. Delegate to password-spray-agent with the specific creds and services.

No inline shell establishment. Do not call start_process for evil-winrm, ssh, or psexec.py from the orchestrator. When credentials are validated and shell access is needed, spawn the appropriate discovery agent (ad-discovery, linux-discovery, windows-discovery) with the credential context — the agent establishes its own session via shell-server MCP.

No inline browser interaction. Do not use browser-server MCP tools from the orchestrator. Web application interaction (navigating, form filling, exploiting) goes through web-exploit-agent or web-discovery-agent.

If you are unsure whether a command is on the allowed list, it is not. Route to a skill.

Subprocess Cleanup After TaskStop

CRITICAL: TaskStop kills the agent but NOT its child processes.

When an agent spawns long-running tools via the Bash tool (hashcat, nxc, ffuf, nmap, responder, etc.), those processes run in separate process groups. TaskStop terminates the agent's Claude process, but the tools keep running as orphans — consuming CPU, holding file locks, and potentially conflicting with subsequent agents.

After every TaskStop on a skill agent, immediately check for and kill orphaned subprocesses:

# Find orphaned processes from killed agent
ps aux | grep -E 'hashcat|nxc|netexec|ffuf|nmap|responder|mitm6|ntlmrelayx|certipy|bloodhound|manspider|gobuster|feroxbuster|nuclei|sqlmap' | grep -v grep

# Kill them (use the PIDs from the ps output)
kill <pid1> <pid2> ...

# Verify they're gone
ps aux | grep -E '<tool>' | grep -v grep

Do this for EVERY TaskStop — parallel resolution kills, manual agent kills, and cleanup kills. The one-liner pattern:

# Kill all orphaned hashcat processes (example)
pkill -f 'hashcat.*kerberoast' 2>/dev/null || true

Use targeted pkill -f patterns that match the specific command rather than broad tool names, to avoid killing processes from still-running agents.

Subagent Delegation

The orchestrator delegates skill execution to custom domain subagents that have full MCP access to the skill-router and category-specific servers. Each subagent invocation executes one skill and returns — the orchestrator makes every routing decision.

Available subagents: See the Subagent Model table in CLAUDE.md for the full agent→domain→MCP mapping. Use the domain→agent map below to look up the correct agent for any skill.

How to delegate: Spawn the appropriate domain agent via the Agent tool with mode: "bypassPermissions", passing the skill name, target info, and relevant context from state.

Operator live-tail. After spawning any agent, use find to locate its JSONL transcript (do NOT cache the session directory — compactions change it):

find ~/.claude/projects/-$(pwd | tr / - | sed 's/^-//')/*/subagents/ \
  -name "agent-<agentId>.jsonl" 2>/dev/null

For live agent monitoring, use agentsee.

Context passing — do NOT override skill methodology. When routing to a technique agent, pass discovery-phase findings as informational context, not as directives to skip techniques. The skill's methodology determines what to try — the orchestrator provides context, not restrictions.

  • WRONG: "Do NOT attempt PHP webshell uploads — they are blocked by content inspection."
  • RIGHT: "Discovery found: basic PHP content (<?php) is blocked by content inspection. PHP short tags also blocked. The skill's full bypass methodology has not been tested yet."
  • ALSO RIGHT: "Web proxy: http://127.0.0.1:8080. Route all attackbox-originated HTTP(S) traffic for this skill through that listener, including browser_open(proxy=...) and CLI web tooling."

The technique skill contains curated bypass sequences (alternative extensions, config file uploads, magic bytes, polyglots, etc.) that the discovery agent never tested. Telling the agent to skip a technique class defeats the purpose of routing to the skill in the first place.

After every subagent return:

  1. Parse the agent's return summary for new targets, creds, access, vulns, pivots, blocked items
  2. Call structured write tools to record findings (add_target, add_credential, add_vuln, etc.)
  3. Call get_state_summary() and run the Step 4 decision logic
  4. Present the next action(s) to the operator — if 2+ independent paths exist, use Parallel Path Presentation format

Each invocation = one skill. Discovery skills find things and return. The orchestrator decides which technique skill to invoke next. Subagents never load a second skill — they stop at their scope boundary, report findings, and return. The orchestrator uses search_skills() and the domain→agent map to route based on finding descriptions.

Inline fallback: If a custom subagent is not available (agent files not installed), STOP and have the operator fix the issue. Skills are only loaded inline when explicitly requested by the operator.

Domain→Agent Map

See CLAUDE.md § Subagent Model for the full domain→agent map. The map derives the correct agent from the skill's category (returned by search_skills()) and name prefix. New skills route automatically when they follow naming conventions.

Orchestrator Loop

The orchestrator runs a decision loop. Each iteration:

watcher_task_id = None   # track the running watcher

while objectives_not_met:
    summary = get_state_summary()
    analyze: unexploited vulns, unchained access, untested creds, pivot map
    pick highest-value next action → select skill + domain agent
    spawn agent in background with: skill name, target info, context
    if watcher_task_id: TaskStop(watcher_task_id)   # kill stale watcher
    watcher_task_id = spawn event watcher in background (cursor, db path)
    END TURN — user is free to interact

    # Notifications arrive asynchronously:
    # - Watcher fires → process new findings, spawn follow-up + new watcher
    # - Agent completes → Post-Skill Checkpoint, next routing decision
    # - User messages → respond, poll_events() as supplementary check

Each iteration is normally one skill invocation. However, when 2+ viable paths exist, the orchestrator always suggests running them in parallel (see Parallel Path Selection). Agent spawns are always presented to the operator for approval.

Built-in Task Sub-Agents (Warning)

Built-in Task sub-agents (Explore, Plan, general-purpose) do NOT have MCP access and cannot invoke skills. Never use them for target-level work:

  • No scanning or enumeration tools against targets
  • No exploiting vulnerabilities
  • No post-exploitation or privilege escalation

What built-in sub-agents may be used for:

  • Pure research (searching for CVE details, reading documentation)
  • Local processing (parsing scan output, compiling exploits)
  • Anything that does not require skill routing or target interaction

For hash cracking and encrypted file cracking, use the credential-recovery skill (inline) instead of ad-hoc cracking in a built-in sub-agent.

Event Monitoring

All agents write critical discoveries mid-run via state MCP tools. Each write (credential, vuln, pivot, blocked, tunnel) also emits a row to the state_events table. The orchestrator uses a background event watcher to get push notifications when agents find something — zero context burn, and the user stays free to interact while agents work.

Setup: Maintain an event_cursor variable starting at 0.

Background Event Watcher

The watcher script lives at tools/hooks/event-watcher.sh. Args: <cursor> <db_path>. Polls every 5s, debounces 5s, 10-minute timeout.

Spawning: Always TaskStop the previous watcher before spawning a new one.

if watcher_task_id: TaskStop(task_id=watcher_task_id)
watcher_task_id = Bash(
    command="bash tools/hooks/event-watcher.sh <event_cursor> ./engagement/state.db",
    run_in_background=true, description="Event watcher (cursor <N>)"
)

Lifecycle: Spawn after every agent launch. Respawn after every notification with updated cursor (poll for gap events between old exit and new start). Cleanup when all agents complete. One watcher suffices for concurrent agents.

Actionable Event Criteria
Event TypeActionable?Follow-up
vuln w/ "FLAG:"Always — immediateProminent callout (see Flag Capture)
credentialAlwaysAuthenticated enum or spray
vuln (high/critical)When technique skill existsSpawn technique agent
vuln w/ "Vhost discovered:"Always — immediateHosts-file update → spawn new web-discovery agent
vuln (medium/low/info)Display onlyNote for later
pivotWhen destination actionableSpawn appropriate agent
blockedDisplay onlyNote for later

Display as timeline table, present follow-up options via AskUserQuestion. Update event_cursor to highest event ID after each notification.

Supplementary Polling

Also call poll_events(since_id=<event_cursor>) when any agent returns, before routing decisions, and before presenting choices — catches gap events.

Post-Skill Checkpoint

When a skill completes and returns control to the orchestrator:

  1. Poll events: Call poll_events(since_id=<event_cursor>) and display any new findings as a timeline (see Event Monitoring above). Update the cursor.
  2. Parse the subagent's return summary for new findings
  3. Check existing state: Call get_state_summary() to see what's already recorded. The database deduplicates at the DB level, but checking first avoids unnecessary write calls.
  4. Call structured write tools to record state changes:
    • New hosts/ports → add_target() / add_port()
    • New credentials → add_credential()
    • Credential test results → test_credential()
    • Access gained/changed → add_access() / update_access()
    • Vulnerabilities confirmed → add_vuln() / update_vuln()
    • Pivot paths identified → add_pivot()
    • Failed techniques → add_blocked()see retry policy below
    • Retry policy for blocked techniques from discovery agents: Discovery agents (web-discovery, ad-discovery, network-recon, linux-discovery, windows-discovery) perform preliminary testing with basic payloads. They are NOT equipped with the full bypass methodology of technique skills. When a discovery agent reports a technique as blocked (e.g., "PHP upload blocked by content inspection"), always record with retry: "with_context" — never retry: "no". The corresponding technique skill (e.g., file-upload-bypass) has comprehensive bypass methodology (alternative extensions, .htaccess, magic bytes, polyglots, double extensions, etc.) that discovery agents don't test. Only a technique skill can definitively confirm a technique is blocked. Mark retry: "no" only when a technique agent (web-exploit, ad-exploit, linux-privesc, windows-privesc) exhausts its skill's methodology and still fails.
  5. Record tool workarounds: If the agent's return summary mentions a tool-specific workaround (e.g., MSF encoder fix, proxy setting, auth flag), append it to the target's notes via update_target(notes=...). This propagates automatically — all subsequent agents see target notes in get_state_summary(). Keep it to one line (e.g., "MSF: set ReverseAllowProxy true + encoder cmd/echo for cmd payloads").
  6. Record failed approaches as blocked: If the agent was killed (TaskStop) or returned without achieving its stated goal, call add_blocked() for each distinct approach the agent attempted. Extract approaches from:
    • The agent's return summary (for clean returns)
    • TaskOutput(block: false) partial output (for killed agents)
    • The orchestrator's own knowledge of what context was passed to the agent Record each with an accurate retry value:
    • "no" — approach is fundamentally invalid (wrong CVE, patched vuln)
    • "with_context" — approach might work with different parameters or strategy (e.g., different trigger mechanism, different port)
    • "later" — approach needs something not yet available (new creds, different access level) This ensures subsequent agents see prior failures in get_state_summary() and don't repeat dead-end approaches.
  7. Check for new usernames — if the skill returned usernames not previously in state, trigger the Usernames Found hard stop before continuing. This applies to ANY skill that discovers users: network-recon (RPC/LDAP null session), web-discovery (user enumeration), ad-discovery (BloodHound/LDAP), SQLi (user table dump), credential-dumping (SAM/LSASS), or any other source.
  8. Call get_state_summary() and run Step 4 decision logic. Use search_skills() to find the right technique skill based on the finding description — skills no longer name specific next skills.
  9. Present the next action(s) to the operator via AskUserQuestion — always proactively recommend; never wait for the operator to ask "what's next." If 2+ independent paths exist, use Parallel Path Presentation format.
Parallel Path Returns

When a returning agent was part of a parallel run (see Parallel Execution), steps 1–4 above still apply — parse findings, record state, record workarounds. Steps 5–9 are replaced by the Race Resolution procedure. Do not run decision logic or route to the next skill until all parallel agents have completed or been killed.

Skills should NOT chain directly into other skills' scope areas. If a discovery skill finds something outside its scope, it reports findings and returns — the orchestrator records state changes and decides what to invoke next.

Parallel Path Presentation

When presenting parallel paths, show the operator a concise table and default to parallel execution.

Format:

**<N> viable paths** — recommend parallel:

| Path | Skill | Confidence | OPSEC | Notes |
|------|-------|------------|-------|-------|
| A | <skill-name> | high/medium/low | low/medium/high | <brief rationale> |
| B | <skill-name> | high/medium/low | low/medium/high | <brief rationale> |

Then use AskUserQuestion with a single-select question:

  • "Run in parallel (Recommended)" — first to succeed wins, others killed
  • "Path A only — <skill-name>"
  • "Path B only — <skill-name>"
  • (additional paths if more than 2)
  • "Run sequentially" — try each in order, stop when one succeeds

If the operator selects parallel, execute the Parallel Execution procedure. Otherwise, run the selected path(s) sequentially using the normal orchestrator loop.

Invocation Log

Immediately on activation — before scoping or doing any work — log invocation to the screen:

  1. On-screen: Print [orchestrator] Activated → <target> so the operator sees the engagement is starting.

Resuming an Existing Engagement

If engagement/state.db already exists (the user said "resume", "continue", "pick it up", "next steps", "where were we", etc.), skip Step 1 entirely:

  1. Call get_state_summary() to load the full engagement state.
  2. Read engagement/config.yaml if it exists. This is the authoritative source for operator preferences (scan type, web proxy, spray tier, cracking method, callback interface). Print a one-line summary of each configured value. Regenerate derived files if missing:
    • engagement/web-proxy.json and engagement/web-proxy.sh from config.yamlweb_proxy
  3. If config.yaml does not exist (pre-config engagement), fall back to reading engagement/scope.md for the ## Web Proxy section. Offer to run the config wizard to create config.yaml for future resumes.
  4. Print a concise status briefing for the operator: targets, current access, key vulns, active tunnels, blocked paths.
  5. Run the Step 4 decision logic to determine the next action.
  6. Present the recommended next action to the operator and wait for approval before spawning any agents.

Do NOT re-initialize scope, re-create the engagement directory, or re-run init_engagement(). The state database is the source of truth.

Step 1: Scope & Engagement Setup

Define Scope

Gather from the user:

  • Targets: IPs, hostnames, URLs, subnets, or domains in scope
  • Out of scope: Hosts, services, or actions explicitly excluded
  • Credentials: Any provided credentials, tokens, or API keys
  • Rules of engagement: Testing windows, restricted techniques, notification requirements, OPSEC constraints
  • Objectives: What does success look like? Domain admin? Data exfil proof? Specific system access?

CTF Acknowledgement

Hard stop — the operator must acknowledge before proceeding.

Use AskUserQuestion:

Question — CTF disclaimer (single-select):

  • Header: "Disclaimer"
  • Question: "This orchestrator is a CTF solver. It runs fully autonomous agents with no OPSEC considerations. Skills have not been thoroughly reviewed by human eyes. By continuing, you accept responsibility for ensuring you have authorization to test the target and for this tool's actions. Confirm to proceed."
  • Options:
    1. Confirm — Proceed with engagement
    2. Cancel — Abort

If the operator selects Cancel, stop immediately.

Engagement Configuration

After CTF disclaimer, before creating the engagement directory, walk the operator through engagement configuration. This creates engagement/config.yaml which captures operator preferences upfront — eliminating repeated hard stops on resume and allowing faster confirmation when context-dependent decisions arise later.

Shortened here. Read the whole file on GitHub.

Signals

GitHub stars
271
Forks
38
Last commit
Apr 2026
Advanced
Catalog kind
skill
Gateway key
red-run-legacy
Source
github.com/blacklanternsecurity/red-run