* feat(worker): record token, cache, and turn usage per agent * feat: replace model tiers with a single SHANNON_AI_MODEL across five providers * feat(cli): rebuild the setup wizard for provider and model selection * docs: document single-model selection and supported providers * feat(worker): use chat completions for OpenAI behind a custom base URL * feat: add SHANNON_AI_OPENAI_FORMAT to pick the wire API for OpenAI gateways * refactor(cli): drop endpoint path hints from the gateway format picker * feat(worker): enable pi in-session provider retry with retry-after backoff * refactor(worker): hand provider error classification to pi and drop the Anthropic ladders * refactor: remove the subscription retry preset and pipeline config section * fix(worker): validate Bedrock credentials with the same live probe as other providers * feat(worker): render the report from structured findings instead of agent-written markdown * fix(worker): dispose the credential probe session on every path * fix(worker): refuse to replace the assembled report with an empty one * refactor(worker): catch post-processing throws across the whole finalization block * revert(worker): drop the report zero-findings guard * docs(worker): correct the retry split and Bedrock credential claims * docs: regenerate llms-full.txt from current sources * feat(cli): build and run the npx flow from a clone * refactor(cli): flatten the setup summary output * feat(cli): reject runs with more than one provider configured * fix(worker): say a rejected bash call never ran * chore(cli): drop grok-4.3 and gpt-5.6-luna from the setup suggestions * feat(worker): capture structured finding locations for SARIF output * fix(worker): enumerate queue confidence so the report inherits it verbatim * feat(worker): give the reporting phase a mode-specific output schema * feat(worker): emit a SARIF 2.1.0 log for exploitative runs * fix(worker): correct SARIF locations and defer fingerprinting to the upload action * fix(worker): drop the confidence suffix from the analysis-mode summary list * feat(worker): give exploit findings a dedicated code location field * feat(worker): carry structured code locations from the vuln queue to the report * fix(worker): join code locations from the vuln queue instead of re-asking agents * fix(worker): spell out the finding_id to category mapping in the tool schema * feat: drop Google/Gemini as a supported AI provider * fix(worker): stop asking the report agent for code locations * docs: correct the provider list and drop the removed rate-limit settings * docs: add provider cyber safeguards and suggested models per provider * docs: document the SARIF output and the report rating thresholds
24 KiB
CLAUDE.md
AI-powered penetration testing agent for defensive security analysis. Automates vulnerability assessment by combining reconnaissance tools with AI-powered code analysis.
Commands
Prerequisites: Docker, AI provider credentials (.env for local, npx @keygraph/shannon setup or env vars for npx)
Dual CLI
Shannon supports two CLI modes, auto-detected based on the current working directory:
npx (npx @keygraph/shannon) |
Local (./shannon) |
|
|---|---|---|
| Install | Zero-install via npm | Clone the repo |
| Image | Pulled from Docker Hub (keygraph/shannon:latest) |
Built locally (shannon-worker) |
| State | ~/.shannon/ |
Project directory |
| Credentials | ~/.shannon/config.toml (via npx @keygraph/shannon setup) or env vars |
./.env |
| Config | ~/.shannon/config.toml (via npx @keygraph/shannon setup) |
N/A |
| Prompts | Bundled in Docker image | Mounted from ./apps/worker/prompts/ (live-editable) |
Mode auto-detection: local mode activates when env var SHANNON_LOCAL=1 is set by the ./shannon entry point (apps/cli/src/mode.ts). Otherwise npx mode.
npx Quick Start
# Configure credentials (interactive wizard)
npx @keygraph/shannon setup
# Or export env vars directly (non-interactive / CI)
export ANTHROPIC_API_KEY=your-key
# Run
npx @keygraph/shannon start -u <url> -r /path/to/repo
Local (Development) Quick Start
# Setup
echo "ANTHROPIC_API_KEY=your-key" > .env
# Build (auto-runs if image missing)
./shannon build
# Run
./shannon start -u <url> -r my-repo
./shannon start -u <url> -r my-repo -c ./apps/worker/configs/my-config.yaml
./shannon start -u <url> -r /any/path/to/repo
Common Commands
# Setup (npx mode only — one-time credential configuration)
npx @keygraph/shannon setup
# Workspaces & Resume
./shannon start -u <url> -r my-repo -w my-audit # New named workspace
./shannon start -u <url> -r my-repo -w my-audit # Resume (same command)
./shannon workspaces # List all workspaces
# Monitor
./shannon logs <workspace> # Show a scan's live log
./shannon status # Show running scans
# Dashboard: http://localhost:8233
# Stop
./shannon stop # Preserves scan data
./shannon stop --clean # Full cleanup including volumes (confirms first; --yes/-y to skip)
# Version
./shannon version # npx: package version; local: git SHA
# Image management
./shannon build [--no-cache] # Local mode: build worker image
npx @keygraph/shannon uninstall # npx mode: remove ~/.shannon/ (confirms first; --yes/-y to skip)
# Build TypeScript (development)
pnpm run build # Build all packages via Turborepo
pnpm run check # Type-check all packages
pnpm biome # Biome lint + format + import sorting check
pnpm biome:fix # Auto-fix lint, format, and import sorting
Monorepo tooling: pnpm workspaces, Turborepo for task orchestration, Biome for linting/formatting. TypeScript compiler options shared via tsconfig.base.json at the root. All packages extend it, overriding only rootDir and outDir. Shared devDependencies (typescript, @types/node, turbo, @biomejs/biome) are hoisted to the root workspace.
Options: -c <file> (YAML config), -o <path> (output directory), -w <name> (named workspace; auto-resumes if exists), --pipeline-testing (minimal prompts, 10s retries), --debug (preserve worker container after exit for log inspection), --yes/-y (skip the confirmation prompt on stop --clean/uninstall; required for non-interactive use)
Architecture
Monorepo Layout
apps/cli/ — @keygraph/shannon (published to npm, bundled with tsdown)
apps/worker/ — @shannon/worker (private, Temporal worker + pipeline logic)
CLI Package (apps/cli/)
Published as @keygraph/shannon on npm. Contains only Docker orchestration logic — no Temporal SDK, business logic, or prompts. Bundled with tsdown for single-file ESM output.
apps/cli/src/index.ts— CLI dispatcher (setup,start,stop,logs,workspaces,status,build,uninstall,version)apps/cli/src/mode.ts— Auto-detection: local mode ifSHANNON_LOCAL=1env var is setapps/cli/src/docker.ts— Compose lifecycle, image pull/build, ephemeraldocker runworker spawningapps/cli/src/home.ts— State directory management (~/.shannon/for npx,./for local)apps/cli/src/env.ts—.envloading, TOML fallback (npx only) viaapps/cli/src/config/resolver.ts, credential validation, provider-scoped env flag buildingapps/cli/src/model-spec.ts—SHANNON_AI_MODEL(<provider>:<model-id>) parsing; mirrorsapps/worker/src/ai/models.tsapps/cli/src/config/resolver.ts— Cascading config (npx only): env vars →~/.shannon/config.toml(parsed withsmol-toml)apps/cli/src/config/writer.ts— TOML serialization and secure file persistence (0o600)apps/cli/src/commands/setup.ts— Interactive TUI wizard (@clack/prompts) for provider credential setup (npx only)apps/cli/src/paths.ts— Repo/config path resolution (bare name →./repos/<name>, or any absolute/relative path)apps/cli/src/version.ts— Version reporting (npx:package.jsonversion; local:git-<sha>)apps/cli/src/tty.ts— Terminal capability detection:requireInteractiveguard (fails fast off-TTY instead of hanging on a prompt),supportsColorcolor gating (NO_COLOR/FORCE_COLOR), andstdoutIsTerminalfor spinner/cursor outputapps/cli/src/commands/— Command handlersapps/cli/infra/compose.yml— Bundled Temporal compose file for npx modeapps/cli/tsdown.config.ts— tsdown bundler configshannon— Node.js entry point (#!/usr/bin/env node) that delegates toapps/cli/dist/index.mjs
Docker Architecture
Infra (Temporal) runs via docker-compose.yml. Workers are ephemeral docker run --rm containers, one per scan, each with a unique task queue and isolated volume mounts.
docker-compose.yml— Infra only:shannon-temporal(port 7233/8233). Network:shannon-netDockerfile— 2-stage build (builder + Chainguard Wolfi runtime). Uses pnpm. Entrypoint:CMD ["node", "apps/worker/dist/temporal/worker.js"]- No
docker-compose.docker.yml— host gateway handled via--add-hostflag in CLI /etc/hostsforwarding — at worker spawn,forwardEtcHostsFlagsinapps/cli/src/docker.tsreads the host's/etc/hostsand emits one--add-hostflag per valid user-added entry. Loopback IPs (127.x,::1) are rewritten tohost-gateway; IPv6 addresses are bracketed. Disable per-scan viaSHANNON_FORWARD_HOSTS=false. No-op on Windows native (WSL2 reads its own/etc/hostsvia the Linux path).
Worker Package (apps/worker/)
apps/worker/src/paths.ts— Centralized path constants (PROMPTS_DIR,CONFIGS_DIR,WORKSPACES_DIR)apps/worker/src/session-manager.ts— Agent definitions (AGENTSrecord). Agent types inapps/worker/src/types/agents.tsapps/worker/src/config-parser.ts— YAML config parsing with JSON Schema validationapps/worker/src/ai/pi/pi-executor.ts— pi harness integration (agent-level retry disabled so Temporal owns restarts; provider-level retry on, seeapps/worker/src/ai/pi/retry-settings.ts)apps/worker/src/services/— Business logic layer (Temporal-agnostic). Activities delegate here. Key:agent-execution.ts,error-handling.ts,container.tsapps/worker/src/types/— Consolidated types:Result<T,E>,ErrorCode,AgentName,ActivityLogger, etc.apps/worker/src/utils/— Shared utilities (file I/O, formatting, concurrency)
Temporal Orchestration
Durable workflow orchestration with crash recovery, queryable progress, intelligent retry, and parallel execution (5 concurrent agents in vuln/exploit phases).
apps/worker/src/temporal/workflows.ts— Main workflow (pentestPipelineWorkflow)apps/worker/src/temporal/activities.ts— Thin wrappers — heartbeat loop, error classification, container lifecycle. Business logic delegated toapps/worker/src/services/apps/worker/src/temporal/activity-logger.ts—TemporalActivityLoggerimplementation ofActivityLoggerinterfaceapps/worker/src/temporal/summary-mapper.ts— MapsPipelineSummarytoWorkflowSummaryapps/worker/src/temporal/worker.ts— Combined worker + client entry point (per-invocation task queue, submits workflow, waits for result)apps/worker/src/temporal/shared.ts— Types, interfaces, query definitions
Five-Phase Pipeline
- Pre-Recon (
pre-recon) — Source code analysis to build the architectural baseline - Recon (
recon) — Attack surface mapping from initial findings - Vulnerability Analysis (5 parallel agents) — injection, xss, auth, authz, ssrf
- Exploitation (5 parallel agents, conditional) — Exploits confirmed vulnerabilities
- Reporting (
report) — Executive-level security report
Supporting Systems
- Configuration — YAML configs in
apps/worker/configs/with JSON Schema validation (config-schema.json). Supports auth settings (MFA/TOTP), URL/code rule scoping (rules.avoid/rules.focus), run-scope steering (vuln_classes,exploit), free-formrules_of_engagement, and post-hocreportoptions (min_severity,min_confidence,guidance, andsarifto emit a SARIF 2.1.0 log viaapps/worker/src/services/sarif-renderer.ts; exploit-only).code_pathavoid rules are enforced via the@gotgenes/pi-permission-systemextension:apps/worker/src/temporal/activities.ts:syncCodePathDenyRuleswrites a globalpathdeny config once per workflow (apps/worker/src/ai/pi/permission-system.ts:syncPermissionSystemConfig), and the executor loads the extension when that config is present (apps/worker/src/ai/pi/pi-executor.ts), so denies fire across every tool and childtasksession.vuln_classes/exploitscope is locked intosession.jsonon first run; resumes with a different scope fail fast (persistOrValidateRunScope). Credential resolution — local mode: env vars →./.env; npx mode: env vars →~/.shannon/config.toml(vianpx @keygraph/shannon setup) - Prompts — Per-phase templates in
apps/worker/prompts/with variable substitution ({{TARGET_URL}},{{CONFIG_CONTEXT}}). Shared partials inapps/worker/prompts/shared/viaapps/worker/src/services/prompt-manager.ts, including_code-path-rules.txt(focus/avoid[FILE]/[GLOB]routing) and_rules-of-engagement.txt(free-text engagement rules). Whenexploit: false,apps/worker/src/services/findings-renderer.tsdeterministically converts each*_exploitation_queue.jsoninto a*_findings.mdfor report assembly — no LLM in the loop - Agent Harness (pi) — Uses the pi harness (
@earendil-works/pi-coding-agent, requires Node ≥ 22.19) viaapps/worker/src/ai/pi/pi-executor.ts(runPiPrompt→createAgentSession). Retry is split inapps/worker/src/ai/pi/retry-settings.ts: pi's agent-level loop is off so Temporal owns agent restarts, whileprovider.maxRetriesstays on — pi reads theproviderblock independently of theenabledflag — so transport faults are absorbed in-session rather than costing a full agent re-run.maxRetryDelayMsis left at pi's 60s default. One model runs every phase, named bySHANNON_AI_MODEL=<provider>:<model-id>(defaultanthropic:claude-sonnet-4-6).apps/worker/src/ai/models.tsparses the spec — splitting on the first colon only, so Bedrock IDs keep theirs — and resolves it through pi'sModelRuntime. pi ships theCredentialStoreinterface but no in-memory implementation (its own readsauth.jsonfrom disk), soRuntimeCredentialStorein that file supplies one: credentials arrive as env vars in an ephemeral container and must never touch disk.createModelRuntime(providerId, apiKey)builds the runtime;allowModelNetworkstays at its defaultfalseso a scan never blocks on a catalog refresh.resolveModelSelection()is async becauseModelRuntime.create()is. Supported providers (all pi-ai provider ids):anthropic,openai,xai,amazon-bedrock. Each provider's API key env var is declared once inPROVIDER_API_KEY_ENV— Shannon uses each vendor's own variable name (OPENAI_API_KEY,XAI_API_KEY, …), never an invented one; Bedrock's entry isAWS_BEARER_TOKEN_BEDROCK, paired withAWS_REGION, which preflight requires separately as provider config rather than a credential.SHANNON_AI_BASE_URLoverrides the endpoint for any provider (proxies/gateways); the credential is unchanged.pointAtGateway(apps/worker/src/ai/models.ts) applies the one dialect change: behind a base URL,openaifollowsSHANNON_AI_OPENAI_FORMAT(chat-completionsdefault, orresponses). Onchat-completionsit switches the API toopenai-completionsand drops the catalogue's Responses-shapedcompatblock so pi'sdetectCompatderives completions settings; onresponsesthe descriptor is unchanged but for the endpoint.resolveGatewayFormatrejects the variable when the provider is notopenaior no base URL is set, since it cannot take effect there. All other providers keep their API. The CLI mirrors the accepted values inapps/cli/src/model-spec.ts, forwards the variable inCOMMON_FORWARD_VARS, and maps it toopenai.formatin config.toml.buildEnvFlagsforwards only the selected provider's credential into the worker container. The CLI mirrors the parse rule and the provider/credential tables inapps/cli/src/model-spec.ts(it cannot import from the worker package); the two must stay in sync. pi ships no JSON-schema output orTask/TodoWritebuilt-ins, so structured queues are captured via asubmit_exploitation_queuecustom tool (apps/worker/src/ai/queue-schemas.ts), andtask(child sessions scoped toread,grep,find,ls,write, andbash— no nestedtaskor collector tools;CHILD_TOOLSinapps/worker/src/ai/pi/task-tool.ts) +todo_write(apps/worker/src/ai/pi/session-tools.ts) are provided as custom tools; the per-phase collectors are pi custom tools (TypeBoxdefineToolinapps/worker/src/collectors/). Shannon sets no thinking configuration at all — nothinkingLevelis passed to anycreateAgentSessioncall, so pi's own default applies. There is no adaptive-thinking support and noCLAUDE_ADAPTIVE_THINKING/core.adaptive_thinkingsetting. Browser automation viaplaywright-cliwith session isolation (-s=<session>). TOTP generation viagenerate-totpCLI tool. Login flow template atapps/worker/prompts/shared/login-instructions.txtsupports form, SSO, API, and basic auth. On authenticated whitebox scans, thevalidate-authenticationpreflight performs the single real login and saves the browser session toauth-state.jsonin the per-session audit directory (path fromauthStateFile()inapps/worker/src/audit/utils.ts, derived fromgenerateAuditPath()). The validation activity (apps/worker/src/services/validate-authentication.ts) removes any stale file from a prior run before the agent runs and verifies the file parses and contains cookies or storage before the preflight is marked complete;logWorkflowCompletedeletes it when the workflow ends so authenticated cookies don't sit on disk between scans. Agent prompts opt in to session reuse by@include(shared/_shared-session.txt)before their<login_instructions>block — the partial restores the session and falls through to the full login flow if verification fails.vuln-auth/exploit-authomit the include and own their own login - Audit System — Crash-safe append-only logging in
workspaces/{hostname}_{sessionId}/. The run directory's top level holds only the human-facing report (Security-Assessment-Report.md,FINAL_REPORT_FILENAMEinapps/worker/src/paths.ts); everything else — deliverables, per-agent logs, prompts,session.json,workflow.log, and browser artifacts — is nested under a hidden.shannon/internals dir (INTERNAL_DIR) so a customer sees only the report. Audit path helpers route throughgenerateInternalPath(apps/worker/src/audit/utils.ts); the CLI nests the overlay backing dirs under the same.shannon/(apps/cli/src/docker.ts,start.ts).session.json/workflow.logreads use dual-read resolvers (resolveSessionJsonPath,resolveRunFile) that prefer.shannon/and fall back to the legacy run-root layout, so pre-restructure workspaces stay listable (workspaces/logs) without migration. Resuming a pre-restructure workspace upgrades it in place first:migrateLegacyWorkspaceLayout(apps/cli/src/commands/start.ts) renames the flat deliverables/logs/session entries into.shannon/(carrying the deliverables.gitalong) before the overlay dirs are mounted, so resume finds the old checkpoints instead of re-running every agent. The report is surfaced by copying the assembledcomprehensive_security_assessment_report.mdfrom the deliverables dir to the run root (copyReportToRunRootinapps/worker/src/services/reporting.ts). WorkflowLogger (apps/worker/src/audit/workflow-logger.ts) provides unified human-readable per-workflow logs, backed by LogStream (apps/worker/src/audit/log-stream.ts) shared stream primitive - Deliverables — Saved to
.shannon/deliverables/in the target repo via thesave-deliverableCLI script (apps/worker/src/scripts/save-deliverable.ts) - Workspaces & Resume — Named workspaces via
-w <name>or auto-named from URL+timestamp. Resume detects completed agents viasession.json.loadResumeState()inapps/worker/src/temporal/activities.tsvalidates deliverable existence, restores git checkpoints, and cleans up incomplete deliverables. Workspace listing viaapps/worker/src/temporal/workspaces.ts
Development Notes
Adding a New Agent
- Define agent in
apps/worker/src/session-manager.ts(add toAGENTSrecord).ALL_AGENTS/AgentNametypes live inapps/worker/src/types/agents.ts - Create prompt template in
apps/worker/prompts/(e.g.,vuln-newtype.txt) - Two-layer pattern: add a thin activity wrapper in
apps/worker/src/temporal/activities.ts(heartbeat + error classification).AgentExecutionServiceinapps/worker/src/services/agent-execution.tshandles the agent lifecycle automatically via theAGENTSregistry - Register activity in
apps/worker/src/temporal/workflows.tswithin the appropriate phase
Modifying Prompts
- Variable substitution:
{{TARGET_URL}},{{CONFIG_CONTEXT}},{{LOGIN_INSTRUCTIONS}} - Shared partials in
apps/worker/prompts/shared/included viaapps/worker/src/services/prompt-manager.ts - Test with
--pipeline-testingfor fast iteration
Key Design Patterns
- Configuration-Driven — YAML configs with JSON Schema validation
- Progressive Analysis — Each phase builds on previous results
- Harness-First — the pi harness (
@earendil-works/pi-coding-agent) handles autonomous analysis - Modular Error Handling —
ErrorCodeenum,Result<T,E>for explicit error propagation, automatic retry (3 attempts per agent) - Services Boundary — Activities are thin Temporal wrappers;
apps/worker/src/services/owns business logic, acceptsActivityLogger, returnsResult<T,E>. No Temporal imports in services - DI Container — Per-workflow in
apps/worker/src/services/container.ts.AuditSessionexcluded (parallel safety) - Ephemeral Workers — Each scan runs in its own
docker run --rmcontainer with a per-invocation task queue. Temporal routes activities by queue name, so per-scan queues ensure activities never land on a worker with the wrong repo mounted
Security
Defensive security tool only. Use only on systems you own or have explicit permission to test.
Code Style Guidelines
Formatting
Biome handles formatting and linting. Run pnpm biome:fix to auto-fix. Config in biome.json: single quotes, semicolons, trailing commas, 2-space indent, 120 char line width.
Clarity Over Brevity
- Optimize for readability, not line count — three clear lines beat one dense expression
- Use descriptive names that convey intent
- Prefer explicit logic over clever one-liners
Structure
- Keep functions focused on a single responsibility
- Use early returns and guard clauses instead of deep nesting
- Never use nested ternary operators — use if/else or switch
- Extract complex conditions into well-named boolean variables
TypeScript Conventions
- Use
functionkeyword for top-level functions (not arrow functions) - Explicit return type annotations on exported/top-level functions
- Prefer
readonlyfor data that shouldn't be mutated exactOptionalPropertyTypesis enabled — use spread for optional props, not directundefinedassignment
Avoid
- Combining multiple concerns into a single function to "save lines"
- Dense callback chains when sequential logic is clearer
- Sacrificing readability for DRY — some repetition is fine if clearer
- Abstractions for one-time operations
- Backwards-compatibility shims, deprecated wrappers, or re-exports for removed code — delete the old code, don't preserve it
Comments
Comments must be timeless — no references to this conversation, refactoring history, or the AI.
Patterns used in this codebase:
/** JSDoc */— file headers (after license) and exported functions/interfaces// N. Description— numbered sequential steps inside function bodies. Use when a function has 3+ distinct phases where at least one isn't immediately obvious from the code. Each step marks the start of a logical phase. Reference:AgentExecutionService.execute(steps 1-9) andinjectModelIntoReport(steps 1-5)// === Section ===— high-level dividers between groups of functions in long files, or to label major branching/classification blocks (e.g.,// === SPENDING CAP SAFEGUARD ===). Not for sequential steps inside function bodies — use numbered steps for that// NOTE:/// WARNING:/// IMPORTANT:— gotchas and constraints
Never: obvious comments, conversation references ("as discussed"), history ("moved from X")
Key Files
CLI: shannon (entry point), apps/cli/src/index.ts (dispatcher), apps/cli/src/docker.ts (orchestration), apps/cli/src/mode.ts (auto-detection)
Entry Points: apps/worker/src/temporal/workflows.ts, apps/worker/src/temporal/activities.ts, apps/worker/src/temporal/worker.ts
Core Logic: apps/worker/src/session-manager.ts, apps/worker/src/ai/pi/pi-executor.ts, apps/worker/src/ai/pi/permission-system.ts (writes code_path deny rules to the @gotgenes/pi-permission-system global config), apps/worker/src/config-parser.ts, apps/worker/src/services/ (incl. preflight.ts, findings-renderer.ts, reporting.ts), apps/worker/src/audit/
Config: docker-compose.yml, apps/cli/infra/compose.yml, apps/worker/configs/, apps/worker/prompts/, tsconfig.base.json (shared compiler options), turbo.json, biome.json
CI/CD: .github/workflows/release.yml (Docker Hub push + npm publish + GitHub release, manual dispatch)
Package Installation
Package managers are configured with a minimum release age (7 days). Requires pnpm >= 10.16.0. If pnpm install fails due to a package being too new, do not attempt to bypass it — report the blocked package to the user and stop.
Troubleshooting
- "Repository not found" — Pass a bare name (
-r my-repo) for./repos/my-repo, or a path (-r /path/to/repo) for any directory - "Temporal not ready" — Wait for health check or
docker compose logs temporal - Worker not processing — Check
docker ps --filter "name=shannon-worker-" - Reset state —
./shannon stop --clean - Local apps unreachable — Use
host.docker.internalinstead oflocalhost - Container permissions — On Linux, may need
sudofor docker commands