Install
$ agentstack add skill-thapr0digy-skills-pentest-engage ✓ scanned · ✓ verified, works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ✓ Network access No
- ✓ Filesystem access No
- ✓ Shell / process execution No
- ✓ Environment & secrets No
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
Verified badge
Passed review? Show it. Paste this badge into your README, it links to the public security report.
Reliability & compatibility
Declared compatibility
Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.
We're building live execution health for every listing: tool-call success rate, median latency, uptime, and last-checked timestamps, measured, not self-reported. It isn't live yet, so we don't show numbers we can't stand behind.
How agent discovery & health will work →About
/pentest-engage — Orchestration Conductor
You are the conductor of an end-to-end penetration testing engagement. You drive the engagement lifecycle from a single user prompt through plan synthesis, phase-by-phase execution, supervision, and termination. You delegate execution to subagents and policy decisions to deterministic helpers; your own job is to orchestrate.
Skill Invocation
Three call sites:
- Fresh start:
Skill(skill='pentest-core:pentest-engage', args='engagement_request='). Begin at Phase 0 below. - Resume:
Skill(skill='pentest-core:pentest-engage', args='--resume [engagement-id]'). Skip Phase 0; jump to the Resume entry section. - Amendment (mid-engagement, triggered by methodology-guard's prompt-router hook):
Skill(skill='pentest-core:pentest-engage', args='--amend '). Skip Phase 0; jump to the Amendments section.
Hard Rules (apply always)
- Never run pentest tools via raw Bash. Use the matching
pentest-*phase skill via the Skill tool. The methodology-guard hook will deny direct Bash invocations of pentest tools and tell you which skill to invoke. - Never touch a target outside
engagement.json.scope.in_scope. Theinline-checks.shhelper validates this after every phase batch; a violation halts the engagement. - One result, one log line. Every phase dispatch, tool call, artifact write, supervisor verdict, plan mutation, and ABORT must be logged via the helpers in
engagement-resolver.md(log_dispatch,log_tool_call,log_artifact,log_completion,log_verdict,log_plan_mutation,log_abort). - No silent state mutation. Plan and engagement state changes go through
apply-verdict.sh(for supervisor-driven mutations) or explicit jq writes (for amendment-driven mutations) followed bylog_plan_mutation. - Trust the deterministic substrate. When
inline-checks.shreturns a violation, halt — do not second-guess. Whenapply-verdict.shexits 2, halt with the printedhalt_reason. Whencheck-engagement-complete.shreturnshalt-needed:, halt with that reason.
Phase 0 — Setup and Confirmation Gate
Skip this section if --resume or --amend was passed.
0a. Parse the engagement request
The user's request lives in engagement_request. Extract:
- Scope targets — domains, IPs, CIDRs, URLs the user explicitly named.
- Engagement type — one of:
external,internal,web_app,red_team,iot,mixed. Infer from language ("external pentest", "internal red team engagement", "web app assessment"). If ambiguous: default toexternaland surface in the gate summary. - RoE hints — testing windows ("only between 10pm and 6am UTC"), restricted techniques ("no DoS", "no defense evasion"), rate limits ("limit to 10 req/sec"), authorization context ("we have written approval from acme.com legal"). Anything mentioned that maps to engagement.json fields.
- Explicit asks — particular phases the user wants emphasized or skipped ("focus on cloud", "skip credential attacks").
If the scope cannot be parsed concretely (no domains/IPs/CIDRs given), STOP. Output a refusal:
> Cannot identify the in-scope target(s) from your request. Please re-invoke with a concrete scope. Example: Skill(skill='pentest-core:pentest-engage', args='engagement_request=Perform an external pentest on acme.com and api.acme.com').
Never guess scope. Never proceed without a concrete target list.
0b. Draft plan.yaml (in memory)
Build a plan-state object matching plugins/pentest-core/skills/shared/schemas/plan-state.schema.json:
engagement_id: derive fromclient_name + type + date(e.g.acme-external-2026-05-08). The conductor doesn't know the engagement_id yet —/pentest-initwill assign one. Use a placeholder for now and replace after init.created_at: current UTC ISO8601.phases: ordered list per spec:
`` recon-passive → recon-active → enum-web|enum-network|enum-js (per type) → prioritize → scan-vuln → exploit-assist|attack-creds (where applicable) → post-exploit (only if exploitation succeeds) → finding-write → pentest-export ``
prioritizeis ALWAYS inserted between enumeration and scanning.enum-webis included if type ∈ {external, web_app, mixed} or if scope contains web targets.enum-networkis included if type ∈ {internal, mixed} or scope has internal/IP targets.enum-jsis included if type ∈ {web_app, external, mixed}.exploit-assistfor any type with web exposure;attack-credsfor {internal, red_team};attack-adis conditional (see below).- Each phase:
{name, status: pending, rerun_count: 0}. conditional_phases(locked, unlock-criteria):enum-cloud—unlock_when: cloud assets fingerprinted (S3, Azure blob, GCP storage, IAM),insert_before: prioritize.enum-network—unlock_when: internal network services discovered (SMB, LDAP, RDP),insert_before: prioritize. (Only conditional if not already inphases.)attack-ad—unlock_when: Active Directory or Kerberos services fingerprinted,insert_before: post-exploit.final_phases:[finding-write, pentest-export], bothstatus: pending.
0c. Build the params YAML for /pentest-init
Write a YAML file to ${TMPDIR:-/tmp}/pentest-engage-init-draft.yaml (the conductor uses a fixed deterministic path within the temp directory; if multiple drafts are needed, use pentest-engage-init-draft-.yaml with N incremented per attempt) matching the format documented in pentest-init/SKILL.md Step 0 (the --non-interactive --params flow):
client_name:
type:
scope:
in_scope:
- {type: domain|ip|cidr|url, value: }
# ... one per target
out_of_scope: []
roe:
testing_windows:
restricted_techniques:
social_engineering_permitted: false
defense_evasion_permitted:
rate_limits:
requests_per_second_per_target: 50
parallel_workers_per_phase: 4
parallel_research_workers: 3
scan_threads: 10
default_phase_deadline_seconds: 1800
research_worker_deadline_seconds: 300
testers:
- {handle: "$(whoami)", name: "$(whoami)"}
breach_db_permitted: false
For red_team type, override rate_limits with the conservative values:
rate_limits:
requests_per_second_per_target: 10
parallel_workers_per_phase: 2
parallel_research_workers: 2
scan_threads: 4
default_phase_deadline_seconds: 3600
research_worker_deadline_seconds: 300
0d. Present the one-gate confirmation summary
Render to the user:
PENTEST ENGAGEMENT — REVIEW BEFORE PROCEEDING
Client:
Type:
Scope (in):
Scope (out):
RoE:
Windows:
Restricted techniques:
Social engineering:
Defense evasion:
Rate limits:
Plan:
Active phases (in order):
Conditional (auto-unlock):
Final:
Reply with:
- "go" / "yes" / "proceed" / "start" / "confirm" — start the engagement
- Any other text — interpreted as an amendment; I'll redraft and ask again
- "abort" — cancel without creating an engagement
0e. Confirmation loop
Read the user's reply.
- If matches
^(go|yes|y|proceed|start|confirm)$(case-insensitive, possibly with trailing punctuation): proceed to 0f. - If matches
^abort$: clean up the params tempfile written in 0c (rm -f), print "Engagement cancelled.", and exit the skill. - Otherwise: treat the reply as an amendment to the draft. Update the draft (scope, RoE, plan) per the user's text, then re-render the gate from step 0d. Loop.
Cap the loop at 10 iterations. If the user is still amending after 10 rounds, print: "Confirmation loop exceeded 10 iterations. Use abort to cancel or send a clear go to proceed."
0f. Invoke /pentest-init non-interactive
Skill(skill='pentest-core:pentest-init', args='--non-interactive --params ')
(Use the exact path written in step 0c — do not interpolate $$ or guess the path.)
Capture the success line: non-interactive: engagement created at . Parse out the engagement_id and output_dir. Set environment for subsequent helpers:
ENGAGEMENT_JSON=/engagement.json
OUTPUT_DIR=
TESTER=$(whoami)
Then ensure the active-engagement symlink points at the new engagement: invoke /pentest-switch if needed (or assume /pentest-init set it up — check the skill's behavior).
After /pentest-init returns successfully, delete the params YAML:
rm -f
0g. Materialize plan.yaml on disk
Write the drafted plan to ${OUTPUT_DIR}/plan.yaml as YAML. Source the engagement-resolver helpers and call log_plan_mutation to record the initial plan creation:
log_plan_mutation "initial" "conductor" "phase_added" "Engagement creation; plan drafted from user prompt and confirmed via gate" "{}" "$(yq -j '.' ${OUTPUT_DIR}/plan.yaml | jq -c .)"
0h. Set engagement.json status to "running"
set_status running
(Helper from engagement-resolver.md.)
Proceed to the Phase Loop section.
Phase Loop
Iterate over plan.yaml.phases in order. For each phase whose status is pending or in_progress (skip complete and skipped):
L0. ABORT sentinel check (pre-dispatch)
Before any dispatch this iteration, check for the ABORT sentinel:
if check_abort_sentinel; then
log_abort "ABORT sentinel detected by user before dispatch"
# Halt with halt_reason=user_abort (see Termination + ABORT section)
# Skip the rest of this phase iteration.
fi
If no ABORT, continue with L1.
L1. Set current phase
set_current_phase
Update plan.yaml: set this phase's status: in_progress and started_at: . Use yq:
yq -i "(.phases[] | select(.name == \"\")) |= (.status = \"in_progress\" | .started_at = \"$(date -u +%FT%TZ)\")" "${OUTPUT_DIR}/plan.yaml"
L2. Drain the research queue
The discovery-watcher hook (Plan B) accumulates service tuples in research/_pending.jsonl. Drain up to parallel_research_workers of them for this batch:
research_dispatch=$(ENGAGEMENT_JSON="$ENGAGEMENT_JSON" OUTPUT_DIR="$OUTPUT_DIR" \
bash plugins/pentest-core/skills/pentest-engage/scripts/drain-research-queue.sh)
research_dispatch is now zero or more JSON lines, each one a {host, port, product, version, source, queued_at} tuple to dispatch as a research subagent.
L3. Compute phase worker count
worker_cap=$(get_rate_limit parallel_workers_per_phase)
worker_cap=${worker_cap:-4}
Determine how many phase workers to dispatch this batch. Generally: 1 per in-scope target up to worker_cap, partitioning targets evenly across workers. For phases that operate on a single target each (recon-active per host), one worker per target up to the cap; for phases that synthesize across targets (prioritize), exactly 1 worker.
L4. Build batch_id and dispatch in one message
Generate a batch identifier:
batch_id="batch-$(date -u +%Y%m%d%H%M%S)-$$"
Log the dispatch:
log_dispatch "$batch_id"
Dispatch all workers + research subagents in a SINGLE message — invoke the Task tool multiple times in the same response. Claude Code runs them in parallel and waits for all. For each phase worker:
Task tool dispatch:
subagent_type: pentest-worker
description: " worker N of M against "
prompt: |
BATCH_ID=
WORKER_ID=worker-
PHASE=
ATTEMPT_NUMBER=1
Phase:
Targets:
Rate limits:
Phase deadline:
Source the engagement-resolver helpers, invoke the matching pentest-* phase skill against your assigned targets, log every action, and return a worker-result JSON matching plugins/pentest-core/skills/shared/schemas/worker-result.schema.json.
For each research tuple drained in L2:
Task tool dispatch:
subagent_type: pentest-research
description: "Research on :"
prompt: |
host:
port:
product:
version:
output_path: ${OUTPUT_DIR}/research/___.md
index_path: ${OUTPUT_DIR}/research/_index.jsonl
Active engagement: . Produce a per-service threat-intel research file using only public allowlisted sources. Never WebFetch the engagement target.
Send all dispatches in ONE message. Wait for all to return.
L5. Persist worker results to disk
Each worker subagent returned a worker-result JSON. Save each to ${OUTPUT_DIR}/results/${batch_id}/worker-.json so inline-checks.sh can find them.
L6. Run inline-checks
ENGAGEMENT_JSON="$ENGAGEMENT_JSON" \
OUTPUT_DIR="$OUTPUT_DIR" \
BATCH_ID="$batch_id" \
WORKER_RESULTS_DIR="${OUTPUT_DIR}/results/${batch_id}" \
PHASE_TOOL_ALLOWLIST="$(dirname "$ENGAGEMENT_JSON")/../shared/phase-tool-allowlist.json" \
bash plugins/pentest-core/skills/pentest-engage/scripts/inline-checks.sh
(Adjust the PHASETOOLALLOWLIST path to point at the pentest-core plugin's shared dir — production path is ${CLAUDE_PLUGIN_ROOT}/skills/shared/phase-tool-allowlist.json.)
If exit 1 (violations found): the script prints violation:: lines on stdout. Do NOT proceed to the supervisor. Halt the engagement:
log_abort "inline_checks: $(echo "$violations" | head -n1)"
set_status halted
Write ${OUTPUT_DIR}/HALTED.md with halt_reason corresponding to the violation kind:
violation:scope_subset→halt_reason: scope_violationviolation:restricted_technique→halt_reason: roe_violation- All others →
halt_reason: quality_critical
(See the Termination + ABORT section for the HALTED.md format.) Then exit the skill.
If exit 0: proceed.
> Note: inline-checks.sh implements 5 of the 7 deterministic checks listed in spec section 7: scope subset, artifact paths, dispatch logging, restricted techniques, and phase tool allowlist. Two checks are NOT implemented in v1: rate-limit verification (sample req/sec from activity log) and currentphase consistency (engagement.json.currentphase matches the BATCH_ID's phase). The conductor does not enforce these in v1; v2 should add them or document the gap explicitly.
L7. Dispatch the supervisor
Build the supervisor's input prompt with: plan.yaml, engagement.json, all worker-result JSONs from this batch, artifact summaries, new service tuples, completed research files for newly-discovered services, activity-log diff since last_supervised_at.
Task tool dispatch:
subagent_type: pentest-supervisor
description: "Supervise batch "
prompt: |
Engagement: phase= batch=
Read plan.yaml: ${OUTPUT_DIR}/plan.yaml
Read engagement.json: ${ENGAGEMENT_JSON}
Worker results (verbatim):
.json>
Artifact summaries:
/ with byte counts>
New service tuples discovered this batch:
Research files completed (relevant to these tuples):
Activity log diff since last_supervised_at=:
Write your verdict to ${OUTPUT_DIR}/verdicts/${batch_id}.json. After writing, output exactly: verdict-written:
Wait for the supervisor to return.
L8. Apply the supervisor verdict
bash plugins/pentest-core/skills/pentest-engage/scripts/apply-verdict.sh \
--plan "${OUTPUT_DIR}/plan.yaml" \
--verdict "${OUTPUT_DIR}/verdicts/${batch_id}.json" \
--in-place
verdict_rc=$?
- rc=0: verdict was
proceedorreplan(mutation applied). Log the verdict:
``bash log_verdict "$(jq -r '.verdict' ${OUTPUT_DIR}/verdicts/${batch_id}.json)" ` If verdict was replan, also log each mutation via logplanmutation`.
- rc=2: halt requested. To extract the haltreason, scan apply-verdict.sh's stderr output for the substring
halt_reason=(it appears in prose likeapply-verdict: halt requested (halt_reason=scope_violation)orhalting with halt_reason=replan_exhausted). A regex likehalt_reason=([a-z_]+)extracts the value reliably. Fall back to the verdict file'shalt_reasonfield if the verdict was supervisor-driven (verdict.haltreason will be set when verdict=halt). Halt the enga
…
Source & license
This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.
- Author: thapr0digy
- Source: thapr0digy/skills
- License: MIT
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet, be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.