Skip to content

Baldie-dev/ZerophageCLI

Repository files navigation

Zerophage CLI

A terminal-based AI assistant built for CVE research that can read, write, and execute commands against a target directory using a DeepSeek LLM with an agentic ReAct loop.

CLI


Purpose

Zerophage takes a target directory and lets you chat with an AI agent that can read files, run shell commands, and patch source code to investigate and exploit vulnerabilities. The agent uses a streaming chat interface so you see its reasoning and tool activity in real time.

Key goals:

  • Interactive, conversational research — ask the agent to find vulnerability classes, trace data flows, explain risky patterns, or write exploit code.
  • Agentic execution — the model drives its own investigation by issuing tool calls between turns without requiring manual prompting for each step.
  • Transparent operation — a live statistics panel tracks token usage, API calls, context size, discovered issues, model names, and agent status.

Architecture

main.go
 ├── agent/          ReAct agentic loop + configuration loader
 │    ├── agent.go        Conversation history, streaming, tool dispatch, GetTodos()
 │    ├── conversation.go ConversationManager — creates/branches/closes/switches Agent instances; persists index
 │    ├── config.go       Loads prompts/config.json; agent/skill/tool definitions
 │    ├── history.go      Stub (session persistence moved to tools/session.go)
 │    └── logger.go       Timestamped debug logger (writes to conversations_logs/)
 ├── llm/            DeepSeek API client
 │    ├── client.go  HTTP client, streaming (SSE) and non-streaming requests
 │    └── types.go   All API request/response/streaming types; model pricing
 ├── tools/          Tool registry and built-in tools
 │    ├── tools.go   Registry, Subset, Definitions, Execute dispatcher; AllForSession()
 │    ├── exec.go    exec_command tool — runs OS commands without a shell
 │    ├── edit.go    edit_file tool — read/write/patch/insert files
 │    ├── skill.go   load_skill tool — loads skill markdown into context on demand
 │    ├── subagent.go spawn_agent tool — delegates tasks to isolated sub-agent loops
 │    ├── web_search.go web_search tool — DuckDuckGo search + page text extraction
 │    ├── todo.go    Todo tools — in-memory task list, persisted via SessionStore
 │    └── session.go  SessionStore — unified _session.json (history + todos + stats)
 ├── ui/             Bubbletea terminal UI
 │    ├── model.go   Root model, Update/View, multi-conversation state management
 │    ├── commands.go Slash-command registry (/clear, /help, /create, /close, /switch, …)
 │    └── styles.go  Lipgloss styles and layout helpers
 └── prompts/        Embedded prompt assets
      ├── config.json     Agent, skill, and tool definitions
      ├── prompts.go      embed.FS declaration      ├── global.md       Global harness instructions appended to every agent's prompt      ├── agents/
      │    ├── cve_finder.md          Root agent — attack surface mapper, notes.md maintenance, delegation
      │    ├── further_research.md    Web intelligence analyst (≥3 searches per session)
      │    ├── in_depth_sast.md       Deep taint-trace specialist for one specific feature
      │    ├── local_dynamic_testing.md  Payload fuzzer and local script harness builder
      │    └── exploit_developer.md   Minimal local PoC writer
      └── skills/
           ├── web_sast.md         Web application SAST methodology (OWASP Top 10)
           └── web_docker_iast.md  IAST probe injection methodology for Docker-hosted apps

Data Flow

User types message (Ctrl+S)
        │
        ▼
  ui.Model.Update  ──► startStreamCmd
                              │
                              ▼
                      agent.Agent.Stream
                        │  Appends user message to history
                        │  Logs request to conversations_logs/<timestamp>.log
                        │  Builds [system, ...history] message slice
                        │  POST /chat/completions (stream=true)
                        │
                        ├─ reasoning token ──► streamChunkMsg ──► shown dimmed in chat
                        ├─ content token   ──► streamChunkMsg ──► UI viewport update
                        │
                        └─ finish_reason == "tool_calls"
                                │
                                ├─ ToolNotification chunk ──► "⚙ name args" shown in chat
                                │
                                ▼
                     tools.Registry.Execute ×N  (parallel, WaitGroup)
                           (exec_command runs OS command; edit_file reads/writes files)
                                │
                                ▼
                         Tool results appended to history (ordered)
                         NewAPICall chunk ──► UI increments API call counter
                         Next LLM turn starts (loop continues)
                                │
                                ▼
                        Final answer streamed to UI
                              │
                              ▼
                  agent.RecordAssistantReply  (appends to history + logs)

Design

ReAct Agentic Loop (agent/agent.go)

The agent implements a ReAct-style loop entirely inside a single goroutine spawned by Stream():

  1. Build the full message slice: [system] + history.
  2. Send a streaming request to DeepSeek.
  3. Drain the SSE stream, accumulating content tokens and tool-call delta fragments.
  4. If finish_reason is tool_calls, execute all requested tools in parallel (see below), collect the results in the original order, append them as tool role messages, and restart from step 2.
  5. If finish_reason is anything else (e.g. stop), forward the final content and exit — the caller invokes RecordAssistantReply to commit the turn to history.

This keeps the loop self-contained; the UI only sees content tokens via the shared channel and is unaware of intermediate tool-call turns.

Parallel Tool Execution

When the model returns multiple tool_calls in a single response, the agent runs them concurrently using a sync.WaitGroup. Each goroutine writes its result to a pre-allocated []llm.Message slice at its own index (no mutex needed), so the results are always returned to the model in the same order the model requested them. After wg.Wait() the whole slice is appended to history in one shot, and the next LLM turn begins.

The API request sets "parallel_tool_calls": true on every request that includes tools. The root agent system prompt also instructs the model to batch independent tool calls into a single response to take full advantage of this — e.g. reading multiple files or running multiple commands simultaneously rather than one per turn.

Tool System (tools/)

Tools implement the Tool interface:

type Tool interface {
    Name()       string
    Definition() llm.ToolDefinition   // JSON Schema sent to the model
    Execute(ctx context.Context, args json.RawMessage) (string, error)
}

tools.Registry is a map[string]Tool. All() returns every built-in tool; Subset(names) produces the permission-scoped registry used by a given agent. Execute never returns a Go error to the caller — failures are encoded as plain text so the model can observe and react to them.

Built-in tool — exec_command

Runs an OS executable with explicit argument slices (no shell invocation), combining stdout and stderr. Output is capped at 64 KiB; execution time is capped at 30 s by default (max 300 s).

Built-in tool — edit_file

Performs file I/O on behalf of the model. Supports four operations:

Operation Behaviour
read Returns the full file content
write Overwrites or creates a file with the given content
patch Applies an ordered list of search/replace diffs; exact match, falls back to whitespace-normalised fuzzy match
insert Inserts text after a specific line number (0 = before line 1)

All paths are resolved relative to the target working directory; path traversal outside it is rejected.

Built-in tools — Todo system

Eight tools back a persistent task list held in memory and persisted inside .zerophage_session.json alongside conversation history and statistics. This keeps the plan outside the context window and lets the agent rehydrate its state across turns without relying on conversation history.

Tool Description
create_todo Add a task with optional priority (1=high/2=med/3=low), confidence (0–1), and parent_id for subtasks
update_todo Change a task's status (pending/in_progress/completed) and optionally append a progress note
list_todos Dump all todos with IDs, status, priority, confidence, and notes — call this before planning to avoid repeated work
complete_todo Shorthand to mark a task completed
append_note Record a partial finding or observation without changing status
reprioritize_todos Batch-update priorities and re-sort the list via [{id, priority}, …]

Each todo carries a confidence field (0.0–1.0). Low-confidence tasks signal uncertainty and can guide the agent toward replanning or using the smarter model. The UI's ToDo panel (bottom of the Tools column) shows a live [✓]/[→]/[ ] summary refreshed after every completed stream turn.

Built-in tool — report_issue

Records a discovered security vulnerability to the persistent issue store. The Stats panel's Discovered Issues section is refreshed live after every tool-call round.

Argument Values Description
issue_name string Short name for the vulnerability (e.g. SSRF, SQL Injection)
severity critical / high / medium / low / info Risk level
confidence confirmed / tentative / speculative Evidence strength: confirmed = exploit verified; tentative = strong evidence; speculative = pattern match only

The UI displays each issue as [Confirmed] SSRF - Critical. Issues persist across sessions in .zerophage_session.json alongside history and todos.

Built-in tool — update_docker_status

Updates the Docker status shown in the Tools panel. The agent is instructed via the global prompt to call this immediately after any docker build or docker run command.

Argument Values Description
status string Short description, e.g. "running", "build failed", "stopped"
port string (optional) Exposed host port, e.g. "3000". If provided, displayed as running :3000

Built-in tool — load_skill

Loads a named skill's full methodology instructions into the conversation context as a tool result. Skills are defined in prompts/config.json and their markdown files are embedded at compile time. The agent is told which skills are available via a brief addendum appended to the system prompt at every turn — it calls load_skill when analysis falls within a skill domain.

Built-in tool — spawn_agent

Delegates a focused, self-contained task to a named sub-agent. The sub-agent runs its own isolated synchronous ReAct loop (up to 20 iterations), then returns a compact text summary. The sub-agent's intermediate reasoning, tool calls, and failures never enter the orchestrator's context window — only the final answer does. This is the primary mechanism for preventing context pollution on large investigation tasks.

Argument Values Description
agent_name string (must exist in prompts/config.json) The agent definition to instantiate for this task.
task string Full task description for the sub-agent. Must be self-contained: include what to analyse, what to look for, and what format to return.
model "smart" (default) / "quick" Model tier. Use "smart" for judgment, code generation, or exploitation tasks. Use "quick" for structured extraction, bulk classification, or pattern matching.

Isolation and sharing:

  • The sub-agent gets its own context window with no parent history.
  • Shared stores (todos, issues, docker) are inherited, so sub-agent findings appear in the UI in real time.
  • The sub-agent inherits the parent agent's allowed_tools (scoped to its own definition's allowed_tools list).
  • Sub-agents can themselves call spawn_agent for nested delegation.
  • The agent tree in the UI shows active sub-agents as child nodes under the orchestrator, marked [working] during execution and [done] when they complete.

Decision rule — spawn only when all hold:

  1. Task input exceeds ~10k tokens of work (e.g. scanning a full module, auditing a large file set).
  2. Result can be summarised to <~2k tokens.
  3. Task is independent (sub-agent needs no access to your current reasoning state).
  4. You can write a complete, unambiguous task description before spawning.

Built-in tool — web_search

Searches DuckDuckGo and returns the plain-text content of the top result pages. No API key or configuration is required. Uses the html.duckduckgo.com/html/ endpoint, fetches each result URL, strips HTML markup, and truncates each page to 4 000 characters.

Argument Type Description
query string (required) Search query, e.g. "CVE-2024-1234 exploit" or "node.js path traversal bypass".
max_results integer (optional) Number of pages to fetch and return (default 3, max 5).

Global Prompt (prompts/global.md)

global.md contains harness-level instructions that are automatically appended to every agent's system prompt, regardless of which agent is active (including sub-agents). This keeps shared operational context out of individual agent files.

Current sections:

Section Content
Harness Instructions Guidelines on parallel tool calls and the todo management workflow
Sub-Agent Delegation Rules for when and how to use spawn_agent, model selection, and task description guidelines
Docker Status Rule to call update_docker_status after every docker build / docker run
Feature Flags Dynamic block showing current SAST / IAST / DAST enabled state (populated at runtime)

Multi-Conversation System (agent/conversation.go)

The ConversationManager holds an ordered list of Conversation objects, each wrapping an independent Agent instance. This lets you run several investigations in parallel — for example, one session for a code audit and another for live exploit testing — without mixing context.

Command Behaviour
/create [agent_name] Spawns a new Agent with a fresh, empty history and its own session file (.zerophage_session_<timestamp>.json). Immediately switches the UI to the new conversation.
/branch [agent_name] Same as /create but copies the current conversation's full history into the new conversation — useful for exploring an alternative approach without losing context.
/switch <id> Saves the current conversation's full display state (messages, stats, viewport, streaming state) and restores the target conversation's state. Blocked while streaming; use /pause first.
/close Closes the active conversation and frees its resources. The nearest remaining conversation becomes active. Cannot close the last conversation.

Per-conversation isolation:

  • Each conversation has its own history, todoStore, issueStore, sessionStore, and debug log file.
  • Token stats, costs, discovered issues, and todo items shown in the panels always reflect the active conversation only.
  • The Conversations panel (Stats column) shows every open conversation with a live status indicator ([thinking] / [working] / [waiting]) for the active one.
  • Switching does not interrupt an in-progress stream — streaming is only allowed on the active conversation.

Every session opens a timestamped log file under conversations_logs/<YYYY-MM-DD_HH-MM-SS>.log. The verbosity is controlled by the LOG_LEVEL environment variable (default: debug):

Level What is recorded
error Session start/end markers and error events (e.g. session save failures)
info Everything in error plus conversation turns (user messages, final assistant replies) and tool call names/arguments/results
debug Everything in info plus full raw API request and response payloads (model, messages, reasoning content, finish reason, token usage)

Set the level in .env:

LOG_LEVEL=info

Logging failures are non-fatal; the agent continues without a log file if the directory cannot be created or the file cannot be opened.

Runtime Files

The following files are created at runtime and are excluded from version control:

File Location Description
.zerophage_session.json Target directory Conversation 1 session: history, todo list, and runtime statistics
.zerophage_session_<timestamp>.json Target directory Session file for conversations 2, 3, … (created on /create or /branch)
.zerophage_convs.json Target directory Multi-conversation index: maps conversation IDs to their session files; used to restore all conversations on restart
conversations_logs/*.log Repo root Session logs — verbosity controlled by LOG_LEVEL

A .env file in the working directory is loaded at startup; DEEPSEEK_API_KEY must be present there or in the environment. Copy .env.example to .env and fill in your key.

Prompt & Agent Configuration (prompts/)

Agent and skill definitions live in prompts/config.json and are embedded at compile time via embed.FS. Each agent entry specifies:

Field Purpose
name Identifier used at runtime (root_agent is required)
path Path inside the FS to the markdown system prompt
allowed_tools Whitelist of tool names this agent may invoke

Each skill entry specifies:

Field Purpose
name Identifier used with load_skill
description One-line summary shown to the model in the system prompt addendum
path Path inside the FS to the skill's markdown file

Skills (prompts/skills/)

Skills are on-demand instruction sets the agent can load mid-conversation using the load_skill tool. They keep domain-specific methodology out of the base system prompt (saving context tokens) while still making it available when relevant.

Skill Description
web_sast Source code analysis methodology for web applications covering OWASP Top 10 vulnerability classes.
web_docker_iast IAST methodology for Docker-hosted web apps: inject [PROBE:tag] log statements, rebuild the container, and capture internal state via docker logs during DAST to confirm vulnerabilities at runtime.

How it works:

  1. Every system prompt turn includes a brief addendum listing available skills (name + description).
  2. When the agent determines a skill is relevant it calls load_skill("web_sast") — the full markdown is returned as a tool result and enters the context window for the remainder of that session.
  3. New skills are added by creating a markdown file under prompts/skills/ and registering it in prompts/config.json.

The default agent (root_agent / cve_finder) is the attack surface mapper. Its system prompt is agents/cve_finder.md. The target directory path is appended to the system prompt at runtime. Specialist sub-agents are defined in agents/further_research.md, agents/in_depth_sast.md, agents/local_dynamic_testing.md, and agents/exploit_developer.md.

Agent Hierarchy

Agent Role Tools
cve_finder (= root_agent) Broad reconnaissance: maps entry points, data sinks, deps; maintains notes.md; delegates to specialists All
further_research Web intelligence: ≥3 targeted searches per session; CVE/advisory/PoC lookup; structured research report web_search, todos, report_issue
in_depth_sast Exhaustive taint trace of one feature end-to-end; loads web_sast skill; verdict with file:line evidence exec_command, edit_file, load_skill, todos, report_issue
local_dynamic_testing Isolates a function into a local test script; runs full payload matrix + character enumeration; optionally uses Docker probes exec_command, edit_file, load_skill, todos, report_issue, Docker
exploit_developer Writes minimal local PoC proving exploitability under documented preconditions exec_command, edit_file, todos, report_issue, Docker

Terminal UI (ui/)

Built with Bubbletea (Elm Architecture) and Lipgloss.

A full-width banner ("Zerophage") spans the top of the screen. Below it are three horizontal panels:

Panel Content
Chat (fills remaining width) Scrollable conversation viewport + textarea input
Stats (30 cols) Working dir, token counters (cached/non-cached/out), API call count, context size, cost estimate, model names, discovered issues, agent tree
Tools (22 cols) Docker / SAST / IAST / DAST status indicators + to-do items

Chat messages are rendered with distinct styles per role:

Role Appearance
user White, prefixed with You ›
assistant Teal — preceded by a dimmed italic reasoning block when the model uses extended thinking
tool Yellow, prefixed with — shows the tool name and truncated arguments
system Muted grey — used for slash-command output and error notices

Streaming is wired through Bubbletea's command system:

  • startStreamCmd calls agent.Stream and returns a streamStartMsg carrying the channel and active model name.
  • nextChunkCmd reads one chunk from the channel and returns a streamChunkMsg, streamDoneMsg, or streamErrMsg.
  • Synthetic chunk types emitted by the agent goroutine — ToolNotification (tool call started), NewAPICall (new LLM turn after tool execution) — are handled in Update to keep the stats panel accurate without exposing agent internals to the UI.
  • The model appends a placeholder assistant message on stream start and updates it token-by-token, showing a blinking cursor () until done.
  • Message queuing: if the user submits a regular message while the agent is streaming, it is stored in a single-slot queue instead of being dropped. The divider above the textarea changes to ⏳ queued: <preview>. When streaming finishes normally, the queued message is automatically sent as the next request. Issuing /stop discards the queue.

Slash commands (ui/commands.go) are intercepted before a message is sent to the LLM:

Command Effect
/clear Clears chat history and resets agent context
/help Lists all available commands
/reasoning <high|medium|low> Sets the model's reasoning effort level for all subsequent API calls
/agent <name> Switches to a named agent, loading its system prompt and allowed-tool set from prompts/config.json
/pause Signals the agentic loop to halt before its next API call (after tool execution completes)
/resume Unblocks a paused agentic loop so it continues with the next LLM turn
/stop Immediately cancels the in-flight HTTP stream. The partially generated response is shown but not saved to history. Any queued message is also discarded.
/compact Condenses the entire conversation history to key findings via the quick model, shrinking the context window
/create [agent_name] Opens a new independent conversation (optionally with a named agent). Immediately switches to it.
/branch [agent_name] Creates a new conversation pre-loaded with the current conversation's full history. Useful for exploring a different approach.
/close Closes the current conversation and switches to the nearest remaining one. Cannot close the last conversation.
/switch <id> Switches to the conversation with the given numeric ID. IDs are shown in the Conversations panel.
/sast <enable|disable> Enable or disable static analysis (SAST). Persisted to config file.
/iast <enable|disable> Enable or disable interactive analysis (IAST). Persisted to config file.
/dast <enable|disable> Enable or disable dynamic analysis (DAST). Persisted to config file.

LLM Client (llm/)

Thin HTTP wrapper around the DeepSeek /chat/completions endpoint.

  • Complete — synchronous, reads the full response body.
  • Stream — sets stream: true, parses data: ... SSE lines, and forwards StreamChunk values over a channel. The response body is drained inside a goroutine; the caller's context.Context is the cancellation mechanism.

llm/types.go also defines per-model context windows (1 000 000 tokens) and per-1M-token pricing (InputCachePerM, InputNoCachePerM, OutputPerM) used by the stats panel for cost estimation.

Two model slots are configured at startup:

Env var Default Used for
DEEPSEEK_SMART deepseek-v4-pro All reasoning turns
DEEPSEEK_QUICK deepseek-v4-flash Fast/cheap calls (reserved)
LOG_LEVEL debug Log verbosity: error / info / debug

Requirements

Runtime

Requirement Details
Go 1.24.2 or later
DEEPSEEK_API_KEY Required. Set as an environment variable or in a .env file in the working directory.
DEEPSEEK_SMART Optional. DeepSeek model name for reasoning turns. Defaults to deepseek-v4-pro.
DEEPSEEK_QUICK Optional. DeepSeek model name for fast turns. Defaults to deepseek-v4-flash.
LOG_LEVEL Optional. Log verbosity for conversations_logs/*.log. error = errors only; info = errors + conversation + tool entries; debug = full API payloads (default).
Terminal A modern terminal with ANSI color and alt-screen support (e.g. Windows Terminal, iTerm2, Alacritty).

Build Dependencies

Managed via go.mod:

Package Purpose
charmbracelet/bubbletea TUI framework (Elm Architecture)
charmbracelet/bubbles Viewport and textarea components
charmbracelet/lipgloss Terminal styling and layout

Usage

# Set API key
export DEEPSEEK_API_KEY=sk-...

# Run against a target directory
go run . /path/to/target

# Start a fresh session (purges .zerophage_session.json)
go run . --new /path/to/target

# Use a custom config file for default settings
go run . --config zerophage.config.json /path/to/target

# Or build first
go build -o zerophage .
./zerophage /path/to/target
./zerophage --new /path/to/target
./zerophage --config zerophage.config.json /path/to/target

Controls:

Key / Flag Action
Ctrl+S Send message
Ctrl+C Quit
--new Purge the previous session (.zerophage_session.json) before starting
--config <file> Load a JSON config file to set default reasoning effort and agent
/clear Clear chat history and reset agent context
/help List available slash commands

A .env file in the current working directory is loaded automatically; existing environment variables take precedence over values in the file.

Config File (--config)

The optional JSON config file sets startup defaults that apply when no persisted session exists for the target. If a session is already present the persisted values always take priority.

{
  "default_reasoning_effort": "medium",
  "default_agent": "root_agent"
}
Field Values Description
default_reasoning_effort low / medium / high Initial reasoning effort level (overridden by persisted session)
default_agent agent name string Initial active agent (must match a name in prompts/config.json)
sast_enabled true / false Whether SAST analysis is enabled. Default: true (omit field to use default).
iast_enabled true / false Whether IAST analysis is enabled. Default: true (omit field to use default).
dast_enabled true / false Whether DAST analysis is enabled. Default: true (omit field to use default).

A ready-to-use sample is provided at zerophage.config.json in the repository root. If a zerophage.config.json exists in the current working directory it is loaded automatically without requiring --config.

About

AI CLI harness designed for CVE research that can perform various actions using a large language model with an agentic ReAct loop.

Resources

License

Stars

1 star

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors