A state machine around one decision
The agent is a LangGraph StateGraph: 14 nodes joined by labelled edges. Nodes are async Python functions, edges are routing rules, and one typed state object travels between them.
Three families of nodes do the work. Reasoning nodes decide: initialize, think and generate_response. Action nodes run tools, one at a time, in parallel waves, or through a fireteam of sub-agents. Human-in-the-loop nodes come in pairs: the await_* nodes stop the run, and the process_* nodes handle your answer when the next run starts.
The model never chooses the next node. It returns a decision, and a plain Python router reads it and picks the edge. After every node, LangGraph saves the whole state to a checkpoint, which lives in PostgreSQL by default. That one fact explains stop and resume, a browser closed mid-run, a backend restart, and approvals that can wait for hours.
Pausing ends the run. await_tool_confirmation, await_approval and await_question lead straight to END. The graph does not sit and wait inside a node. Your answer starts a new run on the same thread: it enters at START, and initialize sees the pending answer and routes to the matching process_* node.
Typed state, on purpose. A node can only write fields that are declared on AgentState. LangGraph drops anything else when it merges the update. The only way into the agent's memory is through a declared channel.
Before the first thought: initialize
Every operator message enters at START and passes through initialize. Before any reasoning happens, it decides whether the run is a resume, a new objective or a blocked target.
What the classifier returns
The classifier answers with a small typed object. Only attack_path_type, required_phase and the target hints are kept; attack_path_type decides which playbook goes into the prompt (chapter 13).
# example: "Exploit CVE-2021-41773 on 10.10.11.42" { "required_phase": "exploitation", "attack_path_type": "cve_exploit", "secondary_attack_path": "path_traversal", "confidence": 0.9, "reasoning": "A named CVE against a network service: the Metasploit workflow fits.", "detected_service": "http", "target_host": "10.10.11.42", "target_port": null, "target_cves": ["CVE-2021-41773"] }
Anatomy of a think step
think is the only place where the model decides anything. Each step sends one fresh pair of messages: a large system prompt rebuilt from the state, and a short human message that never changes. The model answers with exactly one JSON object.
There is no chat history. The model never sees its earlier replies as a conversation. Each step, the past is rewritten into the prompt, already summarised. Every step is a self-contained (situation → decision) pair: easy to audit, and cheap to cache.
The system prompt is ordered from the most static text to the most volatile, in four zones. Three hidden markers split it. When the model is called through the Anthropic API client with prompt caching on (the default), each of the first three zones gets its own cache breakpoint. Every step still sends the whole prompt, but on a normal step only the live suffix is processed at full price; the zones above it are read from the cache.
The live suffix: what gets appended this step
After the state sections, the code appends up to 15 blocks (a couple with a sub-block), most of them conditional, in a fixed order. The last block is always the skill-fit check: the most recent text gets the most attention, so the question "is this still the right attack class?" comes last on purpose.
One reply, two directions
The answer does two jobs at once. output_analysis looks back: it analyses the output of the tool that ran in the previous step. action, tool_name and tool_args look forward to the next tool. The analysis of step N therefore lives in the JSON of step N+1.
When the JSON is broken. The bad reply and the validation error go back to the model with "fix the error and output EXACTLY ONE valid JSON object". It gets 3 attempts in total (LLM_PARSE_MAX_RETRIES). After that the step ends with a synthetic complete, and so does an LLM call that keeps failing after its transient-error retries, so a broken model can never spin forever.
A think step can make more than one LLM call. Deep Think (chapter 9) may run before the decision, and the exploit-path search (chapter 10) may run after it and rewrite the chosen action. Both add their token counts to the same step.
Seven actions, three ways to act
The decision's action field is the router's input. It has seven possible values. Three of them run tools, each in a different shape.
Use a wave when the jobs are single tool calls that share one analysis, such as reading the graph and searching the web at the same time. A wave runs up to 10 tools concurrently (PLAN_MAX_PARALLEL_TOOLS), and the next think analyses every output together.
Use a fireteam when each angle needs its own chain of reasoning, such as an auth specialist and a header-policy specialist that each iterate. A fireteam uses more LLM calls, and in exchange each angle gets its own focused loop (chapter 11).
Mutex groups. Some tools share state inside the sandbox: metasploit_console and msf_restart share one long-lived msfconsole. A wave that contains two tools from the same group is rejected whole, before anything runs, rather than racing on shared state.
The journey of one tool call
Between the model naming a tool and the tool running, the call passes a chain of checks. Some live in the prompt, some in the graph, some in the executor. Pick a tool and a situation, then send it through.
Tool output is hostile
Everything a tool returns comes from a target that may be hostile. A page can carry text that imitates the prompt: a fake heading, a fake system line, a fake end-of-output marker. RedAmon fences every output with a marker whose id is chosen after the output exists.
1 · The nonce comes last
The id is generated after the output exists, and the tool never sees it. A hostile page cannot print a closing marker it does not know.
2 · Forgeries are defanged
Anything that looks like the marker prefix gets zero-width spaces inserted inside its <<<, shown here as , so it can never match a real marker.
3 · A standing rule
Near the top of the agent's prompt, in tier 0, a standing rule tells the model to treat fenced text as data, never as instructions, and to ignore markers it did not see opened.
The text inside the fence stays verbatim, so the agent still reads payloads exactly. The same fence wraps the older outputs in the step history, the Deep Think inputs, graph data that touches scanner text, and crawled knowledge pages. The single-line form (UNTRUSTED_PREVIEW) is used inside step summaries.
What the agent remembers
The agent's memory is the state object. Each step it is rendered back into the prompt as Attack Chain Progress. In parallel, it is written to EvoGraph, an attack-chain graph in Neo4j that the next session reads.
The prompt never carries a raw log. format_chain_context builds it in a fixed order:
- Findings, de-duplicated and sorted by severity.
- Failed attempts, each with its lesson.
- Decisions and fireteam waves.
- The steps: recent ones in detail, older ones as one-liners.
Facts the model extracts (ports, services, technologies, vulnerabilities, credentials, sessions) merge into target_info. Lists are merged without duplicates, and the primary target is replaced whenever the model reports a new one.
Knowing when it is stuck
An agent that repeats itself is the classic failure. RedAmon asks the model to grade every result, checks the grade against what actually changed, and adds signals computed from the state rather than self-reported. It then turns everything into one score with escalating responses.
Six verdicts
Every output_analysis carries a productivity verdict and a rationale. new_info and diagnostic_progress must also cite what was new.
One score, five tiers
Every think step computes a continuous productivity score from seven signals. Most are computed by the orchestrator from the state rather than self-reported, such as how many iterations have passed since the engagement state last grew, so the model cannot simply claim them. The weights shift with session age: early sessions tolerate exploration, late ones punish stalling. Exploitation punishes repeated attempts harder. This calculator runs the real formula.
Axis lock-in: the same dial, turned again
For expensive attempts (credential brute force, directory fuzzing, automated SQLi), an axis extractor reduces each call to the dimensions it holds constant. Three Hydra runs against user admin with three different wordlists collapse onto one axis. The ledger therefore sees the loop even when it is spread over twenty iterations. Each unproductive attempt on the same axis adds the axis weight to the score: at iteration 20 in exploitation, three attempts alone reach 10.2, the critical tier. From red upwards the prompt demands a different hypothesis class, endpoint or credential, not a bigger wordlist.
Uniform responses are inconclusive, not negative. When at least 3 of the last 25 steps share the same failure class and the same body-size bucket (32 bytes), and each finished in under 200 ms, a warning is added to the next prompt. The probes most likely never reached the layer under test: a shell-quoting error, a network error, or a 5xx returned almost instantly. Ordinary 4xx answers do not count. The vector must not be marked as tested on that evidence.
Deep Think: stopping to re-plan
Deep Think is a second LLM call that runs before the decision, when the situation calls for a new strategy. Its only job is strategy, so it never picks a tool. The result is rendered into every following prompt until a new Deep Think replaces it.
Situation
What is known, in a few sentences.
Competing hypotheses
Explanations for the evidence, each with its supporting evidence and one probe that tells them apart. At least two are required once a finding is 60% confident or more. The prompt then demands that the next action be one of those probes: a list of guesses becomes an experiment.
Vectors · approach · priority · risks
The attack vectors seen, the recommended approach, an ordered priority list, and the risks with their mitigations.
Exploit-path search (LATS)
The normal loop follows one idea at a time. Exploit-path search turns exploitation into a tree search inspired by Monte Carlo Tree Search (the LATS research pattern). It proposes several probes, fires them together as one parallel wave, scores each real response, pushes the scores up the branch, and sends the next wave to whichever branch is warming up.
It is not a new node. It is one hook inside think, after the model's reply. On an active turn it evaluates last turn's probes using the model's own response_class labels. It then checks for an exit, selects the best place to search with UCT and expands it into concrete probes. Finally it overrides the action with the next probes: one tool call, or a plan_tools wave with at most one probe per mutex group. They flow through the same router, approval gate and executor as any other tool call. Selection, scoring and backpropagation are pure arithmetic; only expansion calls the LLM.
Shadow mode versus drive mode. When the feature is on, it drives by default: its proposals become the real probes, and the finished tree (its 40 highest-value nodes, with their lessons) is carried into the agent's context. In shadow mode it still builds and expands its own tree, and those expansion calls still cost tokens, but it only scores its proposals against what the agent actually ran with the same tool. It never changes the action and never feeds the tree back; the agent cannot see it.
Fireteam: parallel specialists
When an objective splits into independent angles that each need several steps of reasoning, the root agent can deploy a fireteam: a few specialist sub-agents (five at most by default) running in parallel inside the same process. Each runs its own small ReAct graph.
deploy_fireteam builds one isolated state per member: a snapshot of the parent's target info, phase and skill, a read-only copy of its findings, failures, decisions and recent steps, the sibling missions (marked out of scope), and the member's own mission and toolbox. The deploy node starts one task per member under a semaphore. The members interleave on one event loop, with no message bus and no talking to each other.
fireteam_collect merges every member's target-info delta into the parent and appends their findings, each tagged with the specialist that found it. The root then thinks again with the merged picture.
The member graph · 5 nodes
Bounded
At most 5 members and 5 running at once by default, 10 iterations per member and a wall-clock limit per wave. A member that runs out of iterations ends as partial and keeps its findings.
No recursion, frozen phase
A member cannot deploy its own fireteam, change phase, ask you a question or switch skill; any of those is turned into complete. Its tools are scoped to its mission: a tool outside that toolbox is challenged once for a stated reason, then allowed.
Same rails
Members use the same executor, so phase gates, Rules of Engagement and dangerous-tool approvals all apply. Each member parks on its own approval card: the members already running carry on, the parked one keeps its concurrency slot, and an unanswered card is rejected after 10 minutes.
The human in the loop
The operator can steer at any point. Some controls are pauses built into the graph. Others act on a running graph without stopping it.
Skills: swappable expertise
A skill is a playbook injected into the prompt. Choosing one changes the playbook inside the tools section and the attack-skill block, both of which live in tier 2 of the prompt. The code stays the same.
Built-in attack skills · 14
Written in code, each with a step-by-step workflow, many with their own settings. Each one can be switched on or off per project. The classifier picks from the enabled ones, and they drive the tool guidance.
User attack skills · up to 20 on
Markdown playbooks a user uploads, or imports from the community catalogue. They are off until a project turns them on, and they compete in the classifier as user_skill:<id>.
Chat skills · on demand
Reference docs (frameworks, protocols, tools) injected with /skill <name>. During a run they travel through the guidance queue as [CHAT SKILL: name] and leave the attack class alone. Sent with a new message, they are prepended to the objective that the classifier reads.
Changing class mid-run
The skill-fit check closes every prompt. If the live evidence fits a different enabled class better, the action this turn must be switch_skill. The new class is validated: it must be an enabled built-in skill, an enabled user_skill:<id> or a <term>-unclassified. A rejected switch is written into the step history so the model sees it. The router then sends the run straight back to think, and the next prompt re-renders tier 2 with the new playbook.
When a switch_skill lands on a concrete class while the agent is still in informational, the phase flips to exploitation on its own, but only if the project does not require approval for exploitation. With the default settings nothing flips: the agent must request transition_phase itself, and that waits for your approval.
Plugin rewrites of skill text. A plugin can propose a new version of a built-in skill's static text. It reaches the prompt only after a person approves it, and only while it still matches the factory text it was based on (checked by hash). Every part of the skill must pass validation: no hidden characters, no fake prompt headings, no untrusted-output fences or cache markers, and a size cap. If any part fails validation, the whole skill falls back to the factory text.
Workspace and long jobs
Every project has a sandboxed folder that the agent and every fireteam member work in. It is how big outputs stay out of the prompt and how slow scans avoid blocking a step.
/workspace/<projectId>/ ├── notes/ the agent's scratch (fs_write, fs_edit) ├── tool-outputs/ big outputs land here automatically │ 2026-10-23T09-41-12Z-execute_nuclei.txt ├── jobs/ background runs: <id>.log + .meta.json └── uploads/ your inbox: listed in the prompt
24 fs_* tools read, write, edit with undo, search and archive inside that root. Every path is resolved and rejected if it escapes the root. Archive extraction refuses zip-slip, escaping links and zip bombs.
5 job_* tools run any tool as a background task, so a long ffuf or nuclei run does not hold up a wave that waits for its slowest step. job_spawn checks the phase of the tool it wraps, so it cannot be used to run a tool the phase forbids. Up to 16 jobs run at once across the agent, fewer when memory is tight.
Safety, layer by layer
No single check is trusted alone. The layers overlap on purpose: code catches what the prompt cannot enforce, and the prompt explains what the code would otherwise only refuse. Some limits live only in the prompt, and each layer says where it is enforced.
Be the harness
Twelve situations from real runs. You play the orchestrator: decide what the code does next.