OpenCode plugin for autonomous penetration testing via Docker-based security tools.
bun run build # Bundle with bun + emit declarations with tsc
bun run clean # rm -rf dist
bun run typecheck # tsc --noEmit (type-check only, no output)
bun run test # bun test (no test files exist yet)
bun run clean && bun run build # Full clean rebuildBuild is two-step: bun build src/index.ts --outdir dist --target bun --format esm then tsc --emitDeclarationOnly.
There are no tests, no linter, and no formatter configured. Rely on tsc --noEmit for correctness.
src/
index.ts Plugin entry — registers all tools and hooks
types.ts Shared interfaces (ShannonToolInput, ShannonToolOutput, etc.)
system-prompt.ts System prompt constant injected into chat
tools/ One directory per tool (14 tools total)
hooks/ Hook implementations (authorization, progress, session)
config/ Zod-based config schema + loader
docker/ DockerManager singleton class
commands/ CLI command definitions
skills/ Markdown skill files (pentest guides)
Full tools have 2–4 files:
src/tools/shannon-recon/
index.ts Barrel re-export (one line)
tools.ts Factory function(s) returning ToolDefinition
types.ts Tool-specific arg/result interfaces (optional)
constants.ts Description string constants (optional)
Simple tools (logic-audit, cloud-recon, api-fuzzer) put everything in index.ts.
- Target/Module: ESNext, ESM (
"type": "module"in package.json) - Module resolution:
bundler - Strict mode: enabled (
strict: true) - verbatimModuleSyntax: enabled — use
import typefor type-only imports - noUncheckedIndexedAccess: enabled — indexed access returns
T | undefined - noImplicitOverride: enabled
- Types:
bun-types(not@types/node)
- Indentation: 2 spaces
- Quotes: double quotes (
"...") - Semicolons: omitted (no trailing semicolons)
- Trailing commas: yes, in multi-line objects/arrays/params
- Max line length: not enforced, but keep readable (~100-120 chars)
Order (separated by blank line when mixing groups):
- External packages (
@opencode-ai/plugin,zod,picocolors) - Internal absolute (
../../types,../../docker) - Local relative (
./tools,./types,./constants)
import { tool, type ToolDefinition } from "@opencode-ai/plugin"
import type { ShannonToolInput } from "../../types"
import { SHANNON_RECON_DESCRIPTION } from "./constants"Use import type { ... } for type-only imports — enforced by verbatimModuleSyntax.
Use import * as fs from "fs" for Node builtins (some files use node: prefix, but this is inconsistent — match the file you're editing).
| Element | Convention | Example |
|---|---|---|
| Tool factory | createShannonXxx |
createShannonRecon() |
| Hook factory | createShannonXxxHook |
createShannonProgressTrackerHook() |
| Tool name (registered) | snake_case |
shannon_recon, shannon_idor_test |
| Interfaces/Types | PascalCase |
ShannonToolOutput, DockerExecutionResult |
| Constants | UPPER_SNAKE_CASE |
SHANNON_RECON_DESCRIPTION |
| Helper functions | camelCase (module-level, not exported) |
formatAutoResults() |
| Directory names | kebab-case |
shannon-auth-session/ |
| Config schema | PascalCase + Schema suffix |
ShannonConfigSchema |
| Zod import alias | always z |
import { z } from "zod" |
Every tool follows this exact factory pattern:
import { tool, type ToolDefinition } from "@opencode-ai/plugin"
export function createShannonXxx(): ToolDefinition {
return tool({
description: "Short description of what the tool does.",
args: {
target: tool.schema.string().describe("The target URL or IP"),
command: tool.schema.string().describe("The command to execute"),
timeout: tool.schema.number().optional().describe("Timeout in ms"),
mode: tool.schema.enum(["manual", "auto"]).optional().describe("Testing mode"),
},
async execute(args) {
const output: string[] = []
// ... build output
return output.join("\n")
},
})
}Key rules:
- Args defined with
tool.schema.*().describe("...")— every arg needs.describe() - Enum args:
tool.schema.enum(["value1", "value2"]) - Object args:
tool.schema.object({ key: tool.schema.string() }).optional() - Return type is always
string(output lines joined with"\n") - Output uses markdown formatting (
##,**,`,---)
Every index.ts is a single-line re-export:
export { createShannonRecon } from "./tools"
export type { ShannonReconArgs } from "./types"// In tool execute functions — return error as string, never throw
try {
// ... operation
} catch (error) {
return `ERROR: ${error instanceof Error ? error.message : String(error)}`
}
// In infrastructure code (docker, config) — throw with descriptive message
throw new Error(`Docker container failed to start: ${stderr}`)
// Non-critical checks (file existence, optional config) — empty catch
try {
const data = fs.readFileSync(path, "utf-8")
} catch {}Tools never throw — they return "ERROR: ..." strings. Infrastructure code throws.
Use picocolors (imported as pc) for console output:
import pc from "picocolors"
console.log(pc.cyan("[ShannonPlugin] Loading...")) // Info/progress
console.log(pc.green("[ShannonPlugin] Done")) // Success
console.log(pc.yellow("[ShannonPlugin] Warning...")) // Warning
console.log(pc.red("[ShannonPlugin] Failed...")) // ErrorPrefix all log lines with [ShannonPlugin].
import { z } from "zod"
const MySchema = z.object({
field: z.string().describe("What this field does"),
optional_field: z.boolean().optional().default(false),
})
type MyConfig = z.infer<typeof MySchema>Use .describe() on every Zod field. Infer types with z.infer<> — don't duplicate as interfaces.
- Singleton services (DockerManager): use
classwithprivatefields - Everything else: plain functions. Tool factories, helpers, hooks — all functions
- Use
Record<string, unknown>for loosely-typed objects, notany - Use
error instanceof Error ? error.message : String(error)in catch blocks - Prefer nullish coalescing
??over||for defaults - Never use
as any,@ts-ignore, or@ts-expect-error - Use
import typefor all type-only imports
New tools are registered in the tools record in src/index.ts:
const tools: Record<string, any> = {
shannon_my_tool: createShannonMyTool(),
}Feature-gated tools use config checks:
if (config.shannon.browser_testing) {
tools.shannon_browser = createShannonBrowser()
}| Package | Purpose |
|---|---|
@opencode-ai/plugin |
Plugin SDK — tool(), ToolDefinition, plugin types |
zod |
Config schema validation |
picocolors |
Terminal color output |
js-yaml |
YAML config parsing |
jsonc-parser |
JSONC config parsing |