@pentesterflow/agent— Human-in-the-loop agentic AI CLI for penetration testers and bug hunters. An open-source terminal assistant for authorized offensive-security work that connects to local or hosted LLMs, plans against a scoped target, runs real pentesting tools behind permission gates, remembers lessons across sessions, and writes evidence-backed findings.
- Package:
@pentesterflow/agent· version0.1.0-dev - License: Apache-2.0 · Node:
>=20 - Language: TypeScript (ESM) · ~24,000 LOC across 143 source files · 57 test files
- Runtime UI: Ink (React for the terminal)
- Binaries:
pentesterflow(the CLI) andpentesterflow-browser-mcp(capture MCP server) - Repo homepage: https://github.com/pentesterflow/agent
- What it is & design philosophy
- High-level architecture
- Startup & the CLI entry point
- The agent loop
- LLM providers
- Tools
- Permission & security model
- Skills system
- Memory, sessions & continuous learning
- Coverage & findings
- Browser / Burp capture & MCP
- Terminal UI (Ink/React)
- Configuration & data paths
- Cross-cutting: redaction, logging, target, update, version
- Slash commands & CLI flags reference
- Build, test & developer workflow
- Directory map
PentesterFlow assists across the full pentest lifecycle — scope → recon → enumeration → validation → coverage → reporting → learning — while keeping the analyst in control.
It is built around three ideas:
- Analyst control — the human approves sensitive actions and decides scope.
- Transparent execution — curl-first, reproducible commands, visible tool calls, saved evidence, audit-friendly JSON-lines logs.
- Operational learning — local project + personal knowledge bases improve future sessions without retraining the model or adding user-facing complexity.
⚠️ Authorized use only. The agent can run shell commands, make HTTP requests, edit files, and process captured traffic after approval. Use it only on systems where you have explicit authorization.
Anti-goals it explicitly fights: hallucinated findings (every confirm_finding should be
backed by reproduction evidence), weak context retention (sessions/compaction/snapshots),
poor tooling (real shell/HTTP/Burp/MCP), and lack of auditability (deterministic local paths).
┌──────────────────────────────────────────────────────────────────────┐
│ src/cli/index.ts — parse flags, load config, wire everything up │
└───────┬──────────────────────────────────────────────────────────────┘
│ builds & injects
▼
┌─────────────────┐ events ┌──────────────────────────────────────┐
│ Agent (loop) │◄───────────►│ Ink/React TUI (src/ui/App.tsx) │
│ src/agent/* │ bridges │ transcript · modals · menus · banner │
└───┬──────┬──────┘ └──────────────────────────────────────┘
│ │
│ │ calls ┌──────────────┐
│ └─────────────────────►│ LLM Client │ ollama/openai-compat/
│ │ src/llm/* │ kimi/groq/openrouter/
│ └──────────────┘ deepseek/gemini/lmstudio
│ executes (permission-gated)
▼
┌──────────────────────────────────────────────────────────────────────┐
│ Tool Registry (src/tools/*) │
│ shell · http · file_* · glob/grep · web_* · ask_user · │
│ confirm_finding · coverage · load_skill · browser_capture_* · MCP │
└──────────────────────────────────────────────────────────────────────┘
│ │ │ │ │
▼ ▼ ▼ ▼ ▼
Skills Findings Coverage Intelligence Browser/Burp
registry store store store capture store
+ SKILL.md ./findings coverage-* scenarios.jsonl + ingest server
The agent loop runs outside React. It communicates with the TUI through an event sink
(AgentEvent) and two "bridge" objects (permission + ask) that publish requests into React
state to surface modals. The CLI entry point owns the wiring: it constructs the LLM client,
tool registry, skill registry, all the stores, the permission prompter, and then mounts the
Ink app.
Key dependencies: ink + react (TUI), @modelcontextprotocol/sdk (MCP), undici
(HTTP with custom TLS dispatcher), zod (config/schema validation), zustand (state stores),
gray-matter (SKILL.md frontmatter), marked/marked-terminal/cli-highlight (markdown
rendering), pino (logging), execa (subprocess), fast-glob (search),
write-file-atomic (crash-safe writes), chalk (colors).
src/cli/index.ts is the orchestrator. Its main() does, in order:
forceColor.jsimported first — setsFORCE_COLORbefore chalk-consuming modules cache their color level.- Parse flags (
parseFlags) — see §15. --version/--helpshort-circuit and exit.- Init logger, install SIGINT/SIGTERM/SIGHUP handlers that trip a root
AbortControllerso MCP shutdowns, in-flight HTTP, and tool execs unwind cleanly. - Load config (
config.load()); on parse failure the bad file is renamed toconfig.json.bad-<ts>and defaults are used. CLI flags override config fields. Provider API keys are pulled from env vars (MOONSHOT_API_KEY/KIMI_API_KEY,GROQ_API_KEY,OPENROUTER_API_KEY,DEEPSEEK_API_KEY,GEMINI_API_KEY) when not set. - Browser MCP handling — opt-in per session via
--browser, never persisted. Stalebrowserentries are stripped from config so it can't auto-enable. - Build the LLM client via
llmFactory.newFromConfig(cfg). - Load skills — walk built-in + project-local + user dirs (
skillSearchDirs), apply persisteddisabled_skills. - Create the target, the permission bridges/holders, and the
YoloPrompter. - Create stores —
FindingsStore('findings'),CaptureStore({maxEntries:5000}), session store (new or--resumeid),CoverageStore('findings/coverage-<id>.json'),IntelligenceStore. Stale session temp files older than 60s are cleaned up. - Register tools — see §6. Includes config-defined command plugins and browser-capture tools.
- Burp bridge —
startBurpBridge()binds the ingest server only when--burpis set (prints URL + a random hex token to give to the Burp plugin). - Spawn MCP servers in parallel (
Promise.allSettledoverdiscoverMCPTools), register discovered tools. - List-and-exit modes —
--list-skills/--list-tools. - First-run picker — if
cfg.tooling_profile === undefined, show the minimal/full tooling picker once and persist the answer. - Construct the
Agentwith client, tools, skills, prompter, session store, target, thinking flag, max steps, compaction threshold, tooling profile, prompt profile, intelligence store, streaming flag. If resuming, callagent.resumeSaved()and build a recap summary. - Live skill reload —
fs.watcheach loaded skill dir; on change, debounce 250 ms, clear + re-walk the registry, re-apply disabled set,agent.rebuildFromSkills(), and surface a one-line "skills reloaded" notice. - Background probes (
runProbes) — (a) tool-calling probe and (b) Ollamanum_ctxdetection; both best-effort, feed banner pills/warnings. - Mount the Ink app (
render(<App .../>)), wiring publisher bridges,applyProvider(live/provider+/modelswaps re-probe),startBurpBridge, etc. - On exit, trip the root abort, close watchers, MCP sessions, and the Burp bridge; print
a
pentesterflow --resume <id>hint.
Provider helper functions in this file map backends to labels, default endpoints, locality
(local vs remote), the effective auto-compact threshold (Groq caps at 5500, Kimi sizes to its
real context window), and the effective prompt profile (Groq + Gemini use compact).
Located in src/agent/. The Agent class implements a plan → act → observe cycle.
Agent.run(userMsg, signal, emit, opts) is the entry point per turn:
- Plan — auto-compact if token count exceeds threshold; build a decision plan
(
buildDecisionPlan) that recommends a skill, assigns a risk level, and renders guidance; emit adecisionevent; expand@pathmentions; inject relevant intelligence context. - Act — loop up to
maxSteps(default 20): build aChatRequestwith history + tool specs, callchat()orchatStream(), strip<think>tags, push the assistant message into history, emitassistant-text/assistant-delta. - Observe — for each tool call: parse args, emit
tool-call, check the active skill'sallowed-toolspolicy (isToolAllowed), execute viatools.execute(), emittool-result(with error + duration).load_skillcalls add toactiveSkillsand emitskill-active. No tool calls → finish + emitdone. Step cap exceeded →MaxStepsError.
Every iteration checks signal.aborted. A makeSafeEmit() wrapper swallows emit errors and
suppresses non-terminal events after abort, so a frozen/crashed UI can't wedge the agent.
Controlled by streamingEnabled and isStreaming(client). Streaming emits assistant-delta
events for live rendering; non-streaming (the --no-stream fallback) is a workaround for
backends whose SSE path drops tool_calls mid-stream.
buildSystemPrompt(opts) composes a base prompt from two axes:
- Prompt profile —
full(~8k tokens: full OWASP Top 10 / API Top 10 / LLM Top 10 checklists, bug-bounty workflow discipline, creative-hunter mindset, tech-stack exploit chains, PortSwigger anchors) vscompact(~2k tokens, used for small-TPM providers like Groq/Gemini). - Tooling profile —
minimal(curl-first, scanners banned by default) vsfull(authorizes ffuf/nuclei/sqlmap/etc. when workload fits).
It also appends, dynamically: thinking guidance (if enabled), the engagement scope section (if a target is set), and the enabled skills' descriptions (disabled skills are hidden). The prompt is rebuilt on profile/skill/target changes and on live skill reload.
- Manual
/compact(Agent.compact) sends the history (credentials redacted, capped at ~22k chars) to the LLM with a structured-summary prompt using fixed headings (Current objective, Target and scope, Decisions and assumptions, Tested surface, Findings and evidence, Files and commands, Credentials and placeholders, Open TODOs, Next best actions). History is replaced with[system_prompt, user_msg_with_summary];learnIntelligence()runs. - Auto-compaction (
autoCompact) triggers before a user message when token count exceedsautoCompactThreshold(default 16000; 0 disables). A circuit breaker disables it after 3 consecutive failures for the rest of the session. - The summary is parsed (
parseCompactionSummary) into aSessionMemoryobject (objectives,findings,tested,files,commands,credentials,todos+ metadata), merged with dedup and per-section caps (24 items; findings & credentials uncapped; items over 240 chars truncated). approxTokens()estimates tokens aslength/4.
decisionPlanner.ts—buildDecisionPlanscores enabled skills against the user message (keyword + name/description matching), computes risk (highif the message names exploit/rce/sqlmap/nuclei/ffuf/masscan…), builds a checklist, and renders guidance. Returnsundefinedfor off-topic messages (no skill, normal risk, unknown target, no workflow terms).events.ts— theAgentEventunion:assistant-text,assistant-delta,tool-call,tool-result,error,compact,decision,skill-active,done, plus theMaxStepsErrorclass.mentions.ts—expandFileMentionsinlines@pathfiles (≤64 KB, sensitive paths refused) into a "# Referenced files" block; also powers UI mention completion (file index capped at 5000 files / 1000 dirs / depth 12; skips.git/node_modules/dist/etc.). The raw user message (un-expanded) is what gets persisted to the session.sanitize.ts—stripThinkingTagsremoves<think>…</think>blocks and dangling closing tags emitted by reasoning models (Qwen, DeepSeek-R1, GLM).
When skills are active, a tool is allowed if: it requires no permission, or every relevant
constraint passes union semantics — any active skill that lists the tool in allowed-tools
permits it; a skill with no allowed-tools inherits all. Tool names are canonicalized
(shell ⇄ BashTool, file_write ⇄ FileWriteTool) before comparison. A blocked tool
yields a tool-result error (not a throw) so the model can self-correct.
src/llm/ abstracts 8 backends behind a small Client interface.
interface Client {
name(): string;
model(): string;
chat(req: ChatRequest, signal?): Promise<ChatResponse>;
}
interface StreamingClient extends Client { chatStream(req, onDelta, signal?): Promise<ChatResponse>; }
interface Pinger { ping(signal?): Promise<void>; } // health check for the status barMessages use { role: 'system'|'user'|'assistant'|'tool', content, toolCalls?, toolCallID?, name? }. Tool calls carry JSON-encoded function.arguments and an optional provider field
for vendor extensions (Gemini thoughtSignature).
newFromConfig(cfg) routes the backend through an exhaustive switch (with a never check),
applies per-provider defaults, and enforces required fields (API keys, base URLs).
| Backend | Default base URL | Default model | Notes |
|---|---|---|---|
ollama (config value '') |
http://localhost:11434 |
user | ND-JSON chat, /api/tags ping |
lmstudio |
http://localhost:1234/v1 |
user | OpenAI client, empty key, stop tokens |
openai-compat |
(required) | user | generic OpenAI |
kimi |
https://api.moonshot.ai/v1 |
kimi-k2.6 |
temp-locked (k2.7-code/k2.6/k2.5), max_completion_tokens, thinking toggle (k2.7-code thinks always-on) |
groq |
https://api.groq.com/openai/v1 |
openai/gpt-oss-20b |
compact prompt, low compaction threshold |
openrouter |
https://openrouter.ai/api/v1 |
openrouter/auto |
extra HTTP-Referer/title headers |
deepseek |
https://api.deepseek.com |
deepseek-v4-flash |
|
gemini |
https://generativelanguage.googleapis.com/v1beta |
models/gemini-3.5-flash |
key as query param, non-streaming |
ollama.ts—POST /api/chat, ND-JSON streaming that accumulates tool calls across chunks (the terminaldone:truechunk often carries emptytool_calls); infers tool calls from markdown-fenced/raw JSON in content when the model doesn't emit structured calls; 10-min timeout; synthesizes call IDs.openai.ts— shared by 6 backends. SSE streaming, accumulates fragmented tool calls by index, streamsreasoning_contentto the UI but never re-injects it. LM Studio gets 13 stop tokens + template-marker trimming. Kimi getsthinking:{type:"disabled"},max_completion_tokens, andtemperatureomitted when locked.gemini.ts—:generateContent?key=…; mapstool→function,assistant→model; system prompt →systemInstruction; tool results →functionResponse; uppercases JSON-schematype; strips Gemini-unsupported schema keywords; preservesthoughtSignature.
models.ts—listModelsper-backend (/api/tags,/models, Gemini/models?key), with recommended-first ordering.providers.ts— Kimi/Moonshot context windows (k2.7-code/k2.6/k2.5 = 256K), temperature-lock and thinking-toggle detection,kimiAutoCompactThreshold(75% of window), known model lists.modelWarnings.ts— warns when local models are < 14b or hosted models < 70b (unreliable tool calls).probe.ts—probeToolSupport(sends a probe tool, checks the model actually emits a call;yes/no/unknown);detectOllamaContextWindow(readsnum_ctxfrom/api/show).errors.ts—BackendError+classifyBackend→model-not-loaded/model-not-found/backend-down/unknown.ids.ts—newCallID()→call_<hex>(random) with a sequential fallback.
src/tools/. Every tool implements name(), description(), schema() (JSON Schema),
requiresPermission(), run(args, signal, prompter), and optional summarize() /
permissionHints(). The Registry maps names → tools, exposes them as LLM function specs,
and runs the permission gate before run().
| Tool(s) | Permission | Purpose & key safety |
|---|---|---|
shell / BashTool |
yes | Run /bin/sh(or bash) -c. Denylist (rm -rf /, fork bombs, mkfs, dd of=/dev/…, shutdown/reboot, find -delete/-exec rm); blocks GNU-only flags for macOS/BSD portability; rewrites grep -P→perl -ne; 5-min default / 30-min max timeout; 32 KB output cap; cache key = (rewritten) command. |
http |
yes | One request via undici (TLS verify off). Absolute or target-relative URL; 256 KB body cap; 60s timeout; no redirects; cache key = origin (scheme+host+port); SSRF gate for private hosts. |
file_read / FileReadTool |
no* | Read UTF-8 (200 KB cap). *Sensitive paths gated inline (symlink-aware). |
file_write / FileWriteTool |
yes | Write/create (mode 0644). Cache key = resolved path; sensitive-path gate. |
file_edit / FileEditTool |
yes | Exact-string replace (replace_all opt); sensitive-path gate. |
GlobTool |
no* | fast-glob discovery; skips node_modules/.git/dist/etc.; no symlinks; sensitive-path gate on base dir. |
GrepTool |
no* | Regex content search; glob filter; 5 MB file cap; 200-match default; sensitive-path gate per file. |
web_fetch |
no | Fetch + strip HTML (40 KB text cap, 30s); SSRF gate (noSessionCache). |
web_search |
no | DuckDuckGo HTML endpoint, unwraps DDG redirect links, top 10 results. |
ask_user |
no | Multi-choice question (2–8 options); auto-injects an "Authorized testing" option when scope-related. |
confirm_finding |
no | Persist a confirmed finding to ./findings/<slug>.md; also pushes a Burp issue. Fields: title, severity (critical/high/medium/low/info), url, impact (+ method/param/payload/response_excerpt/curl/remediation). |
coverage |
no | Track (endpoint, param, vuln_class) tuples; actions mark/list/untested/summary/clear; persisted to findings/coverage-<id>.json. |
load_skill |
no | Materialize a skill playbook into context. |
read_payloads |
no | Read curated payload lists shipped with skills (<skill>/payloads/…); ../ escape blocked. |
read_skill_file |
no | Read any auxiliary file in a skill dir (scripts/templates/data). |
browser_capture_* |
no (except _clear) |
Query captured traffic — status, endpoints, requests, get, snapshot, burp_tasks, burp_issues, clear. |
| MCP tools | yes | Discovered from MCP servers, wrapped as mcp_<server>_<tool> (128 KB result cap). |
| Command plugins | configurable | External binary; JSON args on stdin; 5-min timeout, 128 KB cap. |
* "no" tools still apply an inline sensitive-path gate when touching protected files.
SSRF/private-host protection (privateHost.ts) — shared by http and web_fetch.
gatePrivateRequest() prompts with noSessionCache:true when a host (literal or DNS-resolved)
falls in loopback / RFC1918 / link-local-metadata (169.254) / IPv6 ULA·link-local·mapped
ranges, or localhost names.
Sensitive paths (sensitive.ts) — /etc/shadow, /etc/sudoers, master.passwd, and
home-relative .ssh, .aws, .gnupg, .gcloud, .kube, .docker, .config/gcloud,
.config/op, .netrc, .pgpass, .npmrc, .pypirc, .pentesterflow, and shell/REPL
histories (.bash_history, .zsh_history, .python_history, .mysql_history,
.psql_history). Checked by exact match or directory prefix (so .ssh_other does not
match .ssh), against both lexical and symlink-resolved real paths.
Other tool files: aliases.ts (canonical name mapping + KNOWN_TOOL_NAMES validation),
toolDisplay.ts (friendly labels, primary-arg extraction, compact result views),
mcpServers.ts (browser-MCP opt-in filtering — npx -y @browsermcp/mcp@latest).
- Human-in-the-loop by default. Permission-gated tools prompt the analyst with
allow once / allow session / deny (
Decisiontype). Requests carry{ tool, summary, detail, cacheKey?, noSessionCache? }. - Scoped session caching. "Allow session" caches on
(tool, cacheKey)— approvingcurlto one host doesn't license another; approving one shell command doesn't license the next.noSessionCache:true(SSRF, sensitive paths,browser_capture_clear) forces a re-prompt every time. - YOLO mode (
--yolo/--dangerously-skip-permissions//yolo on) —YoloPrompterauto-approves all prompts toallow-once. The shell denylist still hard-blocks catastrophic commands regardless. A stderr + amber "SuperMode" badge warns the user. - Defense in depth: shell denylist + portability guard, SSRF gate, sensitive-path gate
(symlink-proof), TLS-off only inside the HTTP tool, capture server bound to
127.0.0.1with a timing-safe token and chrome-extension CORS. - Credential redaction before compaction, snapshots, and learning (see §14).
- Auditability: sessions, JSON-lines logs, findings, coverage all written to deterministic local paths.
src/skills/. Skills are Markdown playbooks (<dir>/SKILL.md) with gray-matter
frontmatter:
---
name: webvuln # required, ≤64 chars, must match dir name
description: ... # required, ≤1024 chars — tells the agent when to load it
allowed-tools: # optional (alias: tools) — restricts which tools the skill may call
- shell
- http
disable-model-invocation: false # optional — true = only reachable via /skillname
---
# markdown body … (supports ${SKILL_DIR} placeholder)Discovery order (later wins on name collision):
- Built-in
skills/(shipped) - Project-local
./.pentesterflow/skills/ ~/.pentesterflow/builtin-skills/(installer-managed)- Personal
~/.pentesterflow/skills/ --skills <dirs>/ configskills_dirs
Dotfiles and _-prefixed dirs (e.g. _template/) are skipped. The Registry tracks
enabled/disabled state (persisted as disabled_skills); disabled skills stay listed ([off])
but are hidden from the system prompt and refused by load_skill. LoadSkillTool validates +
materializes a skill's body. Live reload re-walks dirs on file change.
Built-in skills: recon, webvuln, ssrf, ssti, jwt, graphql, race, takeover,
supabase, deserialize (+ _template scaffold and a README.md). Each packages methodology,
payloads, constraints, and curl-first discipline ("real PoC + concrete impact, no theoretical
bugs"). Supporting files: discovery.ts, loadSkill.ts, registry.ts, template.ts
(scaffolding for /skills new), validate.ts (frontmatter validation + conformance tests).
Saved to ~/.pentesterflow/sessions/<uuid>.json:
Crash-safe atomic writes (*.tmp.<hex> → fsync → rename); stale temps > 1 min cleaned at
startup; corrupt files skipped on listing. --resume <id> reloads messages/target/memory and
shows a recap. 5-minute automatic context snapshots go to ~/.pentesterflow/context/*.md
(redacted).
A local Continuous Learning System that improves future sessions with no retraining. Scenarios stored as JSONL:
- Project scope:
./.pentesterflow/intelligence/scenarios.jsonl - Personal scope:
~/.pentesterflow/intelligence/scenarios.jsonl - Plus a small hardcoded builtin seed.
Each IntelligenceScenario = { id, title, category, triggers[], technologies[], lesson, recommendedChecks[], avoidMissing[], source, createdAt, confidence(0–1), scope }.
- Learning runs in the background after turns/compactions.
learnFromTextredacts secrets, then extracts scenarios keyed off the compaction headings (preferences→0.82, decisions→0.74, proven-workflows→0.76, lessons→0.8, tool-config→0.73, next-steps→0.72, finding-patterns→0.78, coverage-gaps→0.7), detects technologies (Node/Express/PM2/nginx/ PostgreSQL/GraphQL/WordPress/Supabase/AWS), and extracts triggers (filenames, URLs, vuln keywords). Saved to both scopes. - Retrieval (
search) tokenizes the query, scores scenarios by weighted field (triggers 8×, title 7×, technology 6×, category/checks 5×, avoidMissing 4×, lesson 2× + confidence bonus), returns top 5, formats as a hidden system-context block. - Dedup by id or
(normalizeKey(title), normalizeKey(category)). Failures are logged, not surfaced as task errors.
Coverage (src/coverage/store.ts) — findings/coverage-<session>.json, a deduped matrix
of CoverageEntry { endpoint(METHOD path), param, vulnClass, status, count, firstSeen, lastSeen, notes? }. Statuses: tried | passed | failed | waf-blocked | skipped. untested()
crosses candidates × vuln classes for /next-style suggestions. Persists as
{version:1, entries:[...]}.
Findings (src/findings/store.ts) — ./findings/<slug>.md. Finding { title, severity, url, parameter?, payload?, method?, responseExcerpt?, impact, curl?, remediation?, createdAt, slug }. Slugs normalize the title (≤64 chars), de-collide with -2, -3. Rendered with
heading + impact/payload/evidence/repro-curl/remediation sections.
httpRequest.ts converts findings/curl into raw HTTP/1.1 requests (findingRequestForBurp,
httpRequestFromCurl) for Burp issue import.
src/browser/. An in-memory CaptureStore (bounded, default 5000 entries) holds:
CapturedRequest— method, url, headers, request/response bodies (64 KB cap), timing, source (webRequest/fetch/xhr/ws),receivedAt.EndpointSummary— method + path-without-query + query/body param names + hit counts.SessionSnapshot— page URL, cookies (incl. HttpOnly), localStorage, sessionStorage,document.cookie(for building authenticated requests).BurpTask/BurpIssue— bounded arrays (1000 each).
Ingest server (server.ts) — binds 127.0.0.1 only; auth via timing-safe
X-Pentesterflow-Token; CORS limited to chrome-extension://; endpoints
POST /ingest|/snapshot|/burp/task|/burp/issues, GET /status|/endpoints|/requests|/burp/*,
DELETE /clear; 4 MiB body cap. Started by --burp [port] (default 9999) or /burp.
Standalone MCP server (mcpServer.ts → pentesterflow-browser-mcp binary) exposes the same
capture data to any MCP client (--port, --max-entries, --log). The companion Burp
plugin lives in burp-plugin/pentesterflow_burp.py: send selected Burp requests into the CLI,
queue scan tasks, and import confirmed findings back into Burp as issues.
src/ui/. The agent runs outside React; state flows in via the event sink and the perm/ask
bridges.
App.tsx— owns global state (useReducer), keymap interception (useInput), and the multi-line input (useTextField). DispatcheshandleSlash()for every slash command.state.ts— theAppStatereducer:transcriptentries (user/assistant/tool-call/ tool-result/system/error/finding/decision),busy+phase,apiReady,activeSkill,pendingPerm/pendingAsk/pendingSkills,yolo,transcriptFilter,runningTool. Handles streaming deltas, collapsible tool output (>16 lines / >1200 chars → Ctrl-O expands), and the Ctrl-F filter cycle.Transcript.tsx— finalized entries print once into native scrollback via Ink<Static>(mouse-wheel reaches full history); the streaming entry renders live separately. Markdown viarenderMarkdown(), with a WeakMap row cache.- Input —
Input.tsx(renderer) +useTextField.ts(cursor/paste/multiline state) +SlashMenu.tsx(command typeahead, Tab-complete) +MentionMenu.tsx(path-aware@filepicker).menuWindow.tsprovides shared 5-row windowing. - Bridges —
permBridge.ts(BridgedPrompter) andaskBridge.ts(BridgedAskPrompter) publish agent requests into React state, mount a modal, and resolve the agent's promise on the user's choice. - Modals —
PermissionModal.tsx(y/a/n with command detail box),AskModal.tsx(multi-choice),SecretInputModal.tsx(masked input),SkillsModal.tsx(toggle skills). - Chrome —
Banner.tsx(one-time launch box: provider/model/endpoint/path + tool-support pill),StatusBar.tsx(spinner, phase, elapsed clock, model/target, context %, memory count, SuperMode badge),FirstRunPicker.tsx(minimal vs full tooling). - Formatting —
markdown.ts(regex inline markdown → ANSI, code highlighting, tables, links),toolResultFormat.ts(shell/HTTP colorizers, MCP envelope extraction, collapsible views),slashItems.ts(command catalog + dynamic skill entries).
Config ~/.pentesterflow/config.json (override via PENTESTERFLOW_CONFIG). Zod-validated
schema (ConfigSchema): backend (default ''), model, base_url, api_key,
skills_dirs[], disabled_skills[], mcp_servers[] ({name, command, args[], env?}),
plugins[] ({name, command, args[], description, schema?, requires_permission}),
session_path, thinking_enabled (false), streaming_enabled (true), max_steps (0 = use
default), auto_compact_threshold (DEFAULT_AUTO_COMPACT_THRESHOLD = 16000), temperature?
(0–2), max_tokens?, tooling_profile? (minimal/full). MCP/plugin command fields are
refined by noShellMeta (reject shell metacharacters — no shell injection via config). Saved
atomically (O_EXCL 0600 → fsync → rename); an invalid file is renamed to *.bad-<ts> and
defaults are used.
Runtime artifacts in the working directory. A live engagement directory also accumulates non-source data the agent produces:
./findings/*.md+coverage-*.json,./.pentesterflow/intelligence/scenarios.jsonl, and any analyst-created recon/loot folders (e.g.recon/,pivot/). These are outputs, not part of the codebase, and are git-ignored or treated as engagement data.
| Path | Contents |
|---|---|
~/.pentesterflow/config.json |
Backend/model/endpoint/disabled-skill settings |
~/.pentesterflow/sessions/*.json |
Saved sessions for --resume |
~/.pentesterflow/context/*.md |
Redacted context snapshots (5-min auto) |
./.pentesterflow/intelligence/scenarios.jsonl |
Project intelligence |
~/.pentesterflow/intelligence/scenarios.jsonl |
Personal reusable intelligence |
~/.pentesterflow/builtin-skills/<name>/SKILL.md |
Installer-managed shipped skills |
~/.pentesterflow/skills/<name>/SKILL.md |
Personal skills |
./.pentesterflow/skills/<name>/SKILL.md |
Project-local skills |
./findings/<slug>.md |
Confirmed findings |
./findings/coverage-<session-id>.json |
Coverage state |
~/.pentesterflow/logs/pentesterflow.log |
JSON-lines logs (rotates at 4 MB) |
~/.pentesterflow/debug/session-*.jsonl |
Opt-in full session debug logs |
- Redaction (
src/redact/redact.ts) — shape-preserving masking for Bearer/Authorization tokens, AWS keys (AKIA/ASIA + secret), GitHub (ghp_…), Stripe (sk_live/test_), OpenAI (sk-), Google (AIza), Slack (xox*), JWTs, genericapi_key/secret/password/token=…, Cookie/Set-Cookie,x-api-key, and wholesale-----BEGIN PRIVATE KEY-----blocks. Mask keeps first/last 2 chars. - Permission (
src/permission/permission.ts) —Prompterinterface,Request/Decisiontypes,YoloPrompter. - Logging (
src/logger/) — pino JSON-lines to~/.pentesterflow/logs/pentesterflow.log(4 MB rotation); optionalsessionDebugJSONL (--debug-session). - Target (
src/target/target.ts) —{ baseURL, name }shared by the HTTP tool and system prompt;/targetupdates propagate live. - Self-update (
src/update/selfUpdate.ts) —/update [version]runs the GitHubinstall.sh/install.ps1(5-min timeout); repo viaPENTESTERFLOW_REPO. - Version (
src/version/version.ts) —VERSIONbaked at build via tsupdefine __BUILD_VERSION__(fallbackdev).
| Command | Purpose |
|---|---|
/help |
Keybindings + command reference |
/provider |
Interactive backend + API key + model picker |
/model <id> · /model list |
Switch / list models |
/plan [objective] |
Plan-only turn (no tool execution) |
/next [objective] |
Coverage-driven next-test suggestions |
/target <url> |
Set/clear the engagement base URL |
/compact |
Summarize into persistent memory |
/memory |
Show current session memory |
/snapshot |
Write a redacted context snapshot now |
/burp [port] |
Start the Burp bridge, print URL + token |
/skills [enable|disable|new <name>] |
Manage / scaffold skills |
/maxsteps <n> |
Per-turn tool-call cap |
/thinking on|off |
Toggle visible reasoning guidance |
/update [version] |
Install latest/pinned release |
/yolo [on|off] |
Toggle auto-approval (labs) |
/reset |
Clear conversation + saved session |
/clear |
Clear on-screen transcript only |
/<skill-name> |
Load a skill into the next turn |
/exit · /quit |
Quit |
--backend <ollama\|lmstudio\|kimi\|groq\|openrouter\|deepseek\|gemini\|openai-compat> ·
--model <id> · --base-url <url> · --api-key <key> · --skills <dirs> ·
--resume <session-id> · --browser · --burp [port] · --browser-ingest [port]
(deprecated alias) · --no-stream · --yolo / --dangerously-skip-permissions ·
--list-tools · --list-skills · --log <path> · --debug-session ·
--debug-session-path <path> · --version/-v · --help/-h.
Enter send · Ctrl-N/Ctrl-J newline · Esc cancel turn / clear input · Ctrl-C quit · ↑/↓ history or cursor · Ctrl-A/Ctrl-E line home/end · Ctrl-O expand truncated output · Ctrl-F cycle transcript filter · mouse-wheel scroll · Tab complete (slash/mention).
- Build:
tsup→ ESM bundle todist/(cli.js,browser-mcp.js), Node 20 target, version baked in, shebang banner; externals:react-devtools-core,yoga-wasm-web,bufferutil,utf-8-validate. - Type-check:
tsc --noEmit(strict,noUncheckedIndexedAccess,noImplicitOverride,noFallthroughCasesInSwitch). - Lint/format: Biome (2-space, 100-col, LF, organize-imports,
useNodejsImportProtocol). - Test: Vitest (
src/**/*.test.{ts,tsx}, node env) — 57 test files including a localtestServer.ts, conformance tests for skills, andink-testing-libraryUI tests. - Scripts:
npm run dev[:burp](tsx),build,test[:watch],typecheck,lint[:fix], andci= typecheck + lint + test + build. - CI (
.github/workflows/ci.yml): matrix over ubuntu/macos × Node 20/22 running typecheck → lint → test → build.release.ymlbuilds/publishes binaries. - Install:
install.sh/install.ps1download the standalone binary (Bun baseline runtime for older x86_64, no AVX2) and verify the published SHA-256.
src/
cli/ index.ts (entry/orchestrator), forceColor.ts
agent/ agent.ts (loop), systemPrompt.ts, decisionPlanner.ts,
events.ts, mentions.ts, sanitize.ts
llm/ factory.ts, client.ts, types.ts, providers.ts,
ollama.ts, openai.ts, gemini.ts, models.ts,
modelWarnings.ts, probe.ts, errors.ts, ids.ts, testServer.ts
tools/ registry.ts, types.ts, shell.ts, http.ts, file.ts,
search.ts, web.ts, finding.ts, coverage.ts, ask.ts,
mcp.ts, mcpServers.ts, plugin.ts, browserCapture.ts,
payloads.ts, skillFile.ts, sensitive.ts, privateHost.ts,
aliases.ts, toolDisplay.ts
ui/ App.tsx, state.ts, Transcript.tsx, Input.tsx, Banner.tsx,
StatusBar.tsx, PermissionModal.tsx, AskModal.tsx,
SecretInputModal.tsx, SkillsModal.tsx, SlashMenu.tsx,
MentionMenu.tsx, FirstRunPicker.tsx, TerminalSize.tsx,
permBridge.ts, askBridge.ts, markdown.ts,
toolResultFormat.ts, slashItems.ts, useTextField.ts,
menuWindow.ts, colorLevel.ts, usePing.ts
skills/ registry.ts, discovery.ts, loadSkill.ts, template.ts, validate.ts
session/ store.ts
coverage/ store.ts
findings/ store.ts, httpRequest.ts
intelligence/ store.ts
browser/ store.ts, server.ts, mcpServer.ts
config/ config.ts
redact/ redact.ts, index.ts
permission/ permission.ts
logger/ logger.ts, sessionDebug.ts
target/ target.ts
update/ selfUpdate.ts
version/ version.ts
ask/ ask.ts
skills/ recon webvuln ssrf ssti jwt graphql race takeover
supabase deserialize _template README.md
ask/ ask.ts (AskPrompter interface satisfied by the TUI)
skills/ recon webvuln ssrf ssti jwt graphql race takeover
supabase deserialize _template README.md
burp-plugin/ pentesterflow_burp.py + README.md
scripts/ build-binaries.sh (standalone-binary build)
install.sh / install.ps1 standalone-binary installers (verify SHA-256)
assets/logo.png · tsup.config.ts / tsconfig.json / vitest.config.ts / biome.json
.github/workflows/ ci.yml, release.yml
Generated from a full read of the codebase. Cross-references: README.md (user-facing),
CHANGELOG.md (history), LICENSE (Apache-2.0).
{ "updated_at": "ISO", "id": "uuid", "target": { "baseURL": "...", "name": "..." } | null, "memory": { "version":1, "compactions":N, "objectives":[], "findings":[], "tested":[], "files":[], "commands":[], "credentials":[], "todos":[], "lastSummary":"...", "lastCompactedAt":"..." } | null, "messages": [ { "role","content","toolCalls?","toolCallID?","name?" } ] }