pentest · diff

git:20260423.c064e23 to git:20260423.f0067ce

27 added, 56 removed. Audit B to B.

---
name: pentest
- description: Security testing and OSINT toolkit wrapping 183+ tools from Z4nzu/hackingtool (nmap, sherlock, amass, subfinder, nuclei, httpx, holehe, maigret, trufflehog, sqlmap, etc.). Use whenever the user asks to recon a target, scan a network, enumerate subdomains, investigate a username or email, test a web app, check for leaked secrets, or any other pentest / OSINT task. Claude Code runs tools locally — native, WSL, or Docker — and only hands off on real errors (hardware missing, stdin prompts, unavailable backend).
+ description: Security testing and OSINT toolkit wrapping 183+ tools from Z4nzu/hackingtool (nmap, sherlock, amass, subfinder, nuclei, httpx, holehe, maigret, trufflehog, sqlmap, impacket, netexec, and more). Use whenever the user asks to recon a target, scan a network, enumerate subdomains, investigate a username or email, test a web app, check for leaked secrets, or any other pentest / OSINT task. Runs locally on any OS via native Bash, WSL, or purpose-built Docker images — the skill picks the right backend automatically.
---
# Pentest / OSINT Skill
- You are Claude Code running locally on the user's machine. You have real Bash, real filesystem, real process execution. **Try tools first; handoff only on actual failure.**
+ Claude Code runs on the user's machine. You have real Bash, real filesystem, real process execution. **Run tools immediately; the runtime wrapper handles environment details for you.**
- ## The try-first rule
+ ## The execution model
- The old version of this skill pre-blocked tools based on capability flags (`requires_sudo`, `requires_gui`, `requires_hardware`). That was wrong for Claude Code — you run locally. Docker containers run as root. GUI apps open on the user's desktop. Hardware attached to the host is reachable through the backend.
+ Every tool runs through `ht_run.py`, which:
- **Always attempt a run before handing off.** `ht_run.py` now:
- - Runs the command on the preferred backend
- - Retries with `sudo -n` if it gets permission_denied on native/wsl
- - Classifies real errors (`no_device`, `not_installed`, `stdin_needed`) and only hands off when they can't be worked around
- - Only pre-blocks `interactive` tools (stdin mid-run — stdin piping can't answer prompts)
+ 1. Reads `ht_env.py` to pick a backend — **native** on Linux/macOS, **WSL** on Windows with a real distro, **Docker** anywhere with Docker Desktop.
+ 2. Looks up a purpose-built Docker image for the tool if one exists (`instrumentisto/nmap`, `projectdiscovery/nuclei`, `caffix/amass`, 20+ more). Falls back to `kalilinux/kali-rolling` for anything not mapped.
+ 3. Executes the command. If permission-denied on native/WSL, auto-retries with `sudo -n`. If an error is classifiable as recoverable, handles it.
+ 4. Returns structured JSON with `status`, `stdout`, `stderr`, `returncode`, and the command that ran.
+ Only one pre-block exists: tools flagged `interactive` (they read stdin mid-run). Bypass with `--force` + `--command` if you have non-interactive args.
+
## Bundled scripts
All scripts live at `${CLAUDE_PLUGIN_ROOT}/scripts/`. Call them with `python ${CLAUDE_PLUGIN_ROOT}/scripts/<script>.py ...`. They emit JSON on stdout.
| Script | Purpose |
|---|---|
| `ht_search.py` | Query the tool index. Filter by `--q`, `--category`, `--tag`, `--capability runnable_by_claude`, `--os linux`. |
| `ht_env.py` | Report host OS, WSL distros, Docker availability, and `preferred_backend`. |
- | `ht_run.py` | Tries a tool. `--command "..."` for full-command override (use when RUN_COMMANDS is empty, e.g. nmap). `--args "..."` for append-args. `--network-host` for LAN scans via docker. `--privileged` for raw sockets / hardware. `--force` to ignore the interactive pre-block. |
+ | `ht_run.py` | Execute a tool. `--command "..."` for a full-command override. `--args "..."` for append-args. `--network-host` for LAN scans via docker. `--privileged` for raw sockets / hardware. `--force` to bypass the interactive pre-block. |
## Golden path
- 1. **Read the ask.** Map it to a workflow if possible — see `reference/workflows.md`.
- 2. **Check the environment once.** `python ${CLAUDE_PLUGIN_ROOT}/scripts/ht_env.py` — tells you which backend will run. If Windows + no WSL + Docker stopped, ask the user to start Docker before any Linux-only work.
+ 1. **Read the ask.** Map to a workflow if possible — see `reference/workflows.md`.
+ 2. **Check the environment once.** `python ${CLAUDE_PLUGIN_ROOT}/scripts/ht_env.py` tells you which backend will run. If Windows + no WSL + Docker stopped, ask the user to start Docker before Linux-only work.
3. **Find candidate tools.** `ht_search.py --q "<keyword>"` to discover tool ids. Don't assume ids.
- 4. **Try running them.**
- - If the tool's RUN_COMMANDS slot is populated → `ht_run.py <tool_id> --args "..."`
- - If RUN_COMMANDS is empty (common — many hackingtool classes have `runnable=False`) → use `--command "<full command>"`. The plugin still picks the right backend and docker image for you.
- - For LAN / gateway scans via Docker → add `--network-host` so the container can see your LAN.
+ 4. **Execute them.**
+ - If RUN_COMMANDS is populated → `ht_run.py <tool_id> --args "..."`
+ - If RUN_COMMANDS is empty (many upstream tools have `runnable=False`) → use `--command "<full command>"`. The wrapper still picks the backend and image for you.
+ - For LAN / gateway scans via Docker → add `--network-host` so the container sees your LAN.
- For raw sockets / hardware → add `--privileged`.
5. **Parse the JSON result:**
- - `status: ok` → summarize the interesting parts of stdout for the user; don't paste the whole output if it's long
- - `status: error` with `returncode != 0` and no handoff → tool ran but failed. Report stderr to the user and decide whether to retry with different args.
- - `status: handoff` → the plugin exhausted what it can do. Emit the handoff block (see below).
+ - `status: ok` → summarize the interesting parts of stdout; don't paste the whole output if it's long.
+ - `status: error` with `returncode != 0` → tool ran but failed. Report stderr and decide whether to retry with different args.
+ - `status: fallback` → see `reference/runtime-fallbacks.md`. Pick the template matching the `reason` field.
- `status: timeout` → raise `--timeout <N>` or break the scan into smaller chunks.
6. **Compose, don't just call once.** OSINT chains: `subfinder → httpx → nuclei`. `holehe → sherlock → maigret`. Feed outputs into the next step.
## Docker image overrides
- `ht_run.py` maps common tools to purpose-built images that are faster and lighter than the fallback `kalilinux/kali-rolling`:
+ `ht_run.py` maps common tools to purpose-built images. These pull faster than Kali and have proper ENTRYPOINTs, so you just pass args:
| Tool | Image |
|---|---|
| NMAP | `instrumentisto/nmap` |
| Nuclei | `projectdiscovery/nuclei` |
| Subfinder / Httpx / Katana | `projectdiscovery/*` |
| Amass | `caffix/amass` |
| TheHarvester | `secsi/theharvester` |
- | Holehe / Maigret | `megadose/holehe`, `soxoj/maigret` |
+ | Holehe / Maigret / SpiderFoot | official images |
| TruffleHog / Gitleaks | official images |
| Sqlmap | `paoloo/sqlmap` |
| Impacket / NetExec | `rflathers/impacket`, `byt3bl33d3r/netexec` |
- These images have the binary as ENTRYPOINT, so `ht_run.py` passes args directly. If you need a tool not in the map, it falls back to `kalilinux/kali-rolling bash -lc <cmd>` — slower first pull, but works for anything Kali can `apt install`.
-
Override with `--docker-image my/image` for one-off swaps.
- ## Handoff (only when you can't do it)
-
- When `ht_run.py` returns `status: handoff`, surface the command to the user in this shape:
-
- ```
- ──── Handoff: <tool title> ─────────────────────
- Reason: <reason>
- Diagnostic: <one-line summary of what failed>
- Run this yourself:
-
- <command>
-
- Paste the output back and I'll continue.
- ─────────────────────────────────────────────────
- ```
-
- Handoff reasons and what they mean:
-
- | Reason | What happened | User action |
- |---|---|---|
- | `interactive` | Tool reads stdin mid-run (index flag) | Run manually, or use `--force` with `--command` to supply non-interactive args |
- | `no_backend` | No Linux runtime — no WSL, Docker stopped | Start Docker Desktop or `wsl --install -d Ubuntu` |
- | `no_device` | Tool reported no wifi adapter / no monitor mode / no USB | Hardware not visible to backend (common on Docker for wifi — needs USB passthrough) |
- | `not_installed` | Binary missing in the backend | Try `ht_run.py <id> --install` or `--docker-image <image-with-tool>` |
- | `sudo_password_needed` | `sudo -n` failed — needs a password prompt | Run manually, or configure passwordless sudo for that command |
- | `interactive_detected` | Ran, but the tool blocked on stdin | Index should be updated; in the meantime, handoff |
- | `no_command` | Index has no RUN_COMMANDS and no `--command` was given | Pass `--command "<full cmd>"` yourself |
-
## Ad-hoc commands (no wrapper)
- For one-off invocations that don't map to a hackingtool tool id — e.g. `curl -I https://target` or `arp -a` — just use Bash directly. You don't need to route everything through `ht_run.py`. Use the wrapper when:
- - The tool is in the hackingtool index and you want the image/backend selection
- - You want the error classification / sudo retry
- - You're chaining many tools and want uniform JSON output
+ For one-off invocations that don't map to a hackingtool tool id — e.g. `curl -I https://target`, `arp -a`, `dig @8.8.8.8 example.com` — just use Bash directly. Use `ht_run.py` when:
+ - The tool is in the index and you want automatic image/backend selection
+ - You want permission-error auto-retry
+ - You're chaining multiple tools and want uniform JSON output
## References
- `${CLAUDE_PLUGIN_ROOT}/skills/pentest/reference/workflows.md` — named playbooks
- - `${CLAUDE_PLUGIN_ROOT}/skills/pentest/reference/handoff-patterns.md` — templates for each handoff reason
+ - `${CLAUDE_PLUGIN_ROOT}/skills/pentest/reference/runtime-fallbacks.md` — templates for each fallback reason