disclaw/docs/development-plan.md
Nick Tabeling 69e0b7d727 initial
2026-04-08 14:21:19 +02:00

46 KiB
Raw Blame History

DisClaw — Entwicklungsplan

Autor: Senior Developer Datum: 2026-04-08 Basis: ARCHITECTURE.md, CLAUDE.md, docs/ai-engineer-analyse.md, docs/cli-feature-answers.md, aktueller Stand unter src/.

Der Plan ist umsetzungsnah. Er legt fest, was gebaut wird, wie, in welcher Reihenfolge, und wo der Code erweiterbar bleiben muss. Zeitangaben gibt es bewusst nicht — nur logische Reihenfolge und Abhängigkeiten.

Revision-Log

  • 2026-04-08: Initialversion.
  • 2026-04-08: AI-Engineer-Antworten eingearbeitet (siehe docs/cli-feature-answers.md). Workspace-Location auf ~/.disclaw/workspaces/ (außerhalb DisClaw-Repo) geändert, Windows-Spawn-Fix via cross-spawn ergänzt, Phase 2 (--resume) freigegeben, .claude/settings.json-Schema und Hooks-Layout präzisiert, Abschnitt 6 durch Verweis auf Antworten-Dokument ersetzt.

1. Status Quo

Was existiert und funktioniert (vermutlich)

  • src/index.ts — Bootstrap, lädt Env und Config, initialisiert DB, startet Bot, Graceful Shutdown.
  • src/config/loader.ts — Lädt .env und disclaw.yaml.
  • src/db/database.tsbetter-sqlite3-Wrapper, workspaces und conversations Tabellen mit Prepared Statements, Migrationen via IF NOT EXISTS, WAL und Foreign Keys aktiv.
  • src/db/schema.sql — SQL-Schema (separat vorhanden, aber Migration läuft aktuell aus dem Inline-String in database.ts, Duplikation).
  • src/bot.ts — Discord Client mit Intents Guilds, GuildMessages, MessageContent. Auto-Erstellung/Adoption des Management-Channels. Slash-Command-Registrierung pro Guild. Basic messageCreate Dispatch.
  • src/router.ts — Channel→Workspace Lookup, naives 2000-Zeichen-Splitting an Line-Breaks, Typing-Indikator alle 5 s, schreibt User- und Assistant-Messages in conversations.
  • src/commands/new-agent.ts — Slash-Command /new-agent mit Name-Validierung (Regex), Channel-Create, Workspace-Erstellung, DB-Insert.
  • src/agent/identity.tsagent.yaml, CLAUDE.md und .claude/settings.json Templates werden beim Create geschrieben.
  • src/agent/runner.ts — Spawnt claude -p "<prompt>" --output-format json, baut History als Freitext, parst JSON-Output in drei Formaten, Timeout, AbortController-Support.

Was kaputt oder problematisch ist

  1. P0 Security: src/agent/runner.ts:187 setzt shell: true — direkter Command-Injection-Vektor über Discord-User-Input (siehe AI-Engineer-Analyse §4.1).
  2. P0 Secret Leak: src/agent/runner.ts:181 vererbt process.env vollständig an den Kindprozess → DISCORD_BOT_TOKEN und andere Secrets sind im Claude-Prozess sichtbar (§4.5).
  3. Kein Queueing pro Channel: Mehrere gleichzeitige Nachrichten im selben Channel starten parallele claude-Prozesse im gleichen cwd. Race Conditions bei Tool-Calls und Session-Updates sind garantiert (§2 "Per-Channel-FIFO-Queue").
  4. Agent-Name-Regex unzureichend gegen Path Traversal: Kein expliziter path.resolve-Containment-Check in new-agent.ts. Regex akzeptiert a-a aber kein explizites Path-Check-Gate existiert (§4.3).
  5. Message-Splitting zerreißt Codeblöcke: router.ts:splitMessage() respektiert keine ``` Fences (§2).
  6. History-Injektion statt --resume: runner.ts:buildPrompt() konkateniert Historie als Freitext — kein Prompt-Caching, falsche Rollenzuordnung (§1).
  7. .claude/settings.json Template ist zu permissiv: allow: ["Read", "Write", "Edit", "Bash", ...] ohne Pfad-Restriktionen und ohne deny-Liste. Kein Hook-Guard.
  8. CLAUDE.md Template hat keine Prompt-Injection-Resistenz-Klausel.
  9. Keine User-Rate-Limits, kein globales Concurrency-Cap.
  10. Keine Tests. Kein devDependencies-Eintrag für ein Test-Framework.
  11. Keine strukturierten Logs. Nur console.log. Schwierig zu filtern/parsen.
  12. Keine .env.example im Repo (zu verifizieren bei ls).
  13. Doppelte Schema-Quelle: src/db/schema.sql und Inline-SCHEMA_SQL in database.ts können divergieren.
  14. Typing-Refresh auf 5 s: Discord-Typing läuft ~10 s, 5 s ist okay aber unnötig häufig; 8 s reicht (§2).

Was komplett fehlt

  • /list-agents, /delete-agent Commands.
  • Session-ID-Persistenz (--resume).
  • Streaming-Output.
  • Attachment-Handling.
  • PreToolUse-Hook / Audit-Log.
  • Profile-basiertes Workspace-Scaffolding (researcher/developer/writer/ops).
  • Per-Channel-Queue, globaler Concurrency-Semaphore.
  • Structured Logger (pino o. ä.).
  • Zod-Validation für Config und CLI-JSON-Output.
  • Tests (Unit und Integration).
  • docs/cli-feature-probe.md — Test-Script, das das tatsächliche Verhalten der installierten claude CLI gegen unsere Annahmen verifiziert.

2. Technische Entscheidungen (locked in)

2.1 Context-Persistenz: Hybrid (--resume primär, DB-Rehydrierung Fallback)

Was: Session-ID pro Workspace in der DB persistieren. Runner ruft claude -p "<msg>" --resume <session-id> --output-format json --bare auf. Wenn keine Session-ID existiert oder --resume fehlschlägt, wird aus den letzten N DB-Einträgen ein Bootstrap-Prompt gebaut und eine neue Session gestartet. Die neue session_id aus dem JSON-Output wird nach jedem Run in der DB aktualisiert (defensiv gegen Fork-Verhalten älterer CLI-Versionen).

Warum: Prompt Caching von Anthropic greift nur bei stabilen Präfixen innerhalb einer Session. Neue Session pro Nachricht = 0 % Cache-Hit, jede CLAUDE.md wird neu tokenisiert. Mit --resume erwartete Einsparung 6090 % Input-Tokens (siehe ai-engineer-analyse.md §1). Bestätigt über cli-feature-answers.md Frage 1+2: -p + --resume ist offiziell dokumentiert, session_id liegt top-level im JSON-Output.

Wie:

  • Neue DB-Spalten in workspaces: claude_session_id TEXT, session_updated_at TEXT. Migration additiv (ALTER TABLE ... ADD COLUMN).
  • Runner-API bekommt einen SessionStore-Typ injiziert (damit der Runner testbar bleibt).
  • Session-ID lesen: JSON.parse(stdout).session_id (top-level). Nach jedem Run per store.set(ws.id, json.session_id) persistieren, auch wenn die ID identisch zur vorigen ist — fängt Fork-Verhalten (--fork-session, CLI-Upgrades, cleanupPeriodDays-Expiry) transparent auf.
  • Fallback bei --resume Fehler: Error-String auf session .* not found oder ähnlich matchen → claude_session_id = NULL setzen und erneut starten ohne --resume (optional mit --continue als zweiter Fallback-Stufe).
  • Die conversations-Tabelle bleibt Source of Truth für Rehydrierung nach Bot-Neustart oder vollständigem Session-Verlust.
  • Metriken: usage-Block aus JSON-Output parsen (cache_read_input_tokens, cache_creation_input_tokens, input_tokens, output_tokens) und pro Run loggen. Exakter Pfad (result.usage.* oder top-level usage.*) wird empirisch via Probe-Script verifiziert.
// src/agent/runner.ts (Skizze)
export interface SessionStore {
  get(workspaceId: number): string | null;
  set(workspaceId: number, sessionId: string): void;
  clear(workspaceId: number): void;
}

async function runOnce(ws: Workspace, msg: string, store: SessionStore): Promise<RunResult> {
  const sid = store.get(ws.id);
  const args = [
    "-p", msg,
    "--output-format", "json",
    "--bare",
    "--append-system-prompt-file", path.join(ws.workspace_path, "CLAUDE.md"),
  ];
  if (sid) args.push("--resume", sid);
  const res = await spawnClaude(args, ws.workspace_path);
  if (res.ok) {
    // Immer updaten — fängt Fork/Cleanup-Edge-Cases ab
    if (res.sessionId) store.set(ws.id, res.sessionId);
    return res;
  }
  if (sid && isResumeFailure(res.error)) {
    store.clear(ws.id);
    return runOnce(ws, msg, store); // einmaliger Retry ohne --resume
  }
  throw res.error;
}

Status: Freigegeben. cli-feature-answers.md Frage 1+2 bestätigt, dass -p "..." --resume <id> --output-format json offiziell dokumentiert ist und session_id als Top-Level-Feld im JSON-Output liegt. Kein Blocker mehr. Das Probe-Script verifiziert lediglich noch die genauen Pfade des usage-Blocks und bleibt als Regression-Check im Repo.

2.2 Prozessmodell: Spawn pro Nachricht (beibehalten)

Was: Ein Bot-Prozess, ein claude-Kindprozess pro Nachricht, terminiert nach Antwort.

Warum: Persistente Prozesse sind im MVP nicht vertretbar (150300 MB RSS pro idle Agent, Supervisor nötig, Windows-Signal-Semantik fragil, interaktiver Modus ist TTY-first). Spawn-pro-Nachricht ist einfacher, isolierter, crash-resistenter. Der Kaltstart-Overhead wird durch --resume + Prompt Caching teilweise kompensiert.

Wie: Bestehender runner.ts-Ansatz, aber mit shell: false, sanitizedEnv, Windows-Pfad-Auflösung, SessionStore, und abstrahiertem Event-Stream-Return-Type, damit Phase 5 (Streaming) ohne API-Bruch nachgerüstet werden kann.

Windows-Spawn-Entscheidung (locked in): cross-spawn als Dependency. Grund: claude wird auf Windows als claude.cmd-Wrapper installiert (%APPDATA%\npm\claude.cmd), nicht als .exe. Seit Node 20.12 (CVE-2024-27980) blockt Node child_process.spawn("foo.cmd", args, { shell: false }) bzw. verlangt explizites Arg-Quoting — spawn("claude", args) scheitert außerdem, weil Node ohne shell: true keine .cmd-Extension im PATH-Lookup anhängt. cross-spawn abstrahiert beides (löst .cmd-Wrapper auf, quoted Args korrekt, identisches Verhalten Linux/Mac/Windows).

Trade-off: Eine Mini-Dependency (~30 kB, keine transitiven Deps) gegen ~50 Zeilen Windows-Workaround-Code (where-lookup + .cmd-Wrapper-Parsing + direkter node cli.js-Spawn). Die Eigenbau-Variante bleibt als Fallback dokumentiert, falls cross-spawn jemals ausfällt, aber ist für den MVP nicht die erste Wahl — der Pragmatik-Gewinn wiegt die Dependency auf.

Zusätzlich: Beim Bot-Start einmalig CLAUDE_PATH aus .env lesen; falls leer, where claude (Windows) / which claude (Unix) automatisch ausführen, Ergebnis in-memory cachen. Fehlermeldung mit klarem Hinweis auf CLAUDE_PATH, falls nicht auffindbar. windowsHide: true, stdio: ["pipe", "pipe", "pipe"], child.stdout.setEncoding("utf8").

2.3 Per-Channel-Queue

Was: FIFO-Queue pro channel_id, die alle Runs im gleichen Channel serialisiert. Parallele Channels bleiben parallel.

Warum: Siehe §1.3 Status Quo — ohne Queue laufen race-anfällige parallele Runs im selben Workspace.

Wie: In-Memory Map<channelId, Promise<void>> im Router. Neue Tasks werden an die letzte Promise der Queue angehängt. Kein externer Queue-Service, kein Redis — MVP-tauglich. Zusätzlich: globaler Semaphore (max. 4 parallele claude-Prozesse) über ein simples Counting-Primitive.

// src/runtime/channel-queue.ts
export class ChannelQueue {
  private queues = new Map<string, Promise<unknown>>();
  enqueue<T>(channelId: string, task: () => Promise<T>): Promise<T> {
    const prev = this.queues.get(channelId) ?? Promise.resolve();
    const next = prev.catch(() => undefined).then(task);
    this.queues.set(channelId, next);
    next.finally(() => {
      if (this.queues.get(channelId) === next) this.queues.delete(channelId);
    });
    return next;
  }
}

2.4 Message-Splitting: Eigenbau, codeblock-aware

Was: Eigene Funktion splitForDiscord(text, limit=1900), die Fences ( ) erkennt, offene Blöcke beim Split schließt und im nächsten Chunk wieder öffnet. Bei Ausgaben >8 k Zeichen: stattdessen als .md-Attachment senden.

Warum: Keine externe Library erfüllt das 1:1 (die üblichen "split-to-discord"-Pakete sind unmaintained oder fehlerhaft bei Fences). Eigenbau ist ~60 Zeilen und testbar. Limit 1900 statt 2000 lässt Puffer für Prefix (1/3).

Wie: Siehe Algorithmus-Skizze in docs/ai-engineer-analyse.md §2 "2000-Zeichen-Splitting". Ablegen unter src/discord/split.ts mit Unit-Tests.

2.5 Workspace-Isolation: Permissions + PreToolUse-Hook

Was: .claude/settings.json bekommt ein Default-Deny für Pfade außerhalb ./ und für gefährliche Bash-Muster. Zusätzlich PreToolUse-Hook, der jeden Tool-Call mit absolutem Pfad gegen workspace_abs_path matched und abbricht, wenn außerhalb.

Warum: Permissions allein reichen nicht, weil Read(./**) sich mit cd .. via Bash aushebeln lässt. Der Hook ist die zweite Verteidigungslinie und gleichzeitig der Audit-Log-Hook.

Wie:

  • src/agent/templates/settings.default.json als Template mit Platzhalter {{workspace_abs}}.
  • Schema-Struktur wie in cli-feature-answers.md Frage 3 bestätigt: Permissions unter permissions.allow / permissions.ask / permissions.deny, mit zusätzlich permissions.defaultMode und permissions.additionalDirectories. Hooks als dreistufige Struktur: hooks.<EventName>[].matcher + hooks.<EventName>[].hooks[].{type, command, if, timeout}.
  • Hook-Script als Node-File: src/agent/hooks/guard-tool.ts, das beim Workspace-Create nach ./.claude/hooks/guard-tool.cjs gerendert wird (compile-to-CJS damit der Hook ohne Build-Step ausführbar ist).
  • Template-Beispiel (das tatsächliche Schema):
    {
      "$schema": "https://json.schemastore.org/claude-code-settings.json",
      "permissions": {
        "allow": ["Read", "Write(./**)", "Edit(./**)", "Bash(git diff *)"],
        "ask": ["Bash(git push *)"],
        "deny": [
          "Read(../**)", "Write(../**)", "Edit(../**)",
          "Read(./.env)", "WebFetch",
          "Bash(rm -rf *)", "Bash(curl * | sh)",
          "Bash(ssh *)", "Bash(scp *)",
          "Bash(* /etc/*)", "Bash(* ~/.ssh/*)",
          "Bash(powershell -Command *)"
        ],
        "defaultMode": "acceptEdits"
      },
      "hooks": {
        "PreToolUse": [
          {
            "matcher": "Bash|Read|Write|Edit",
            "hooks": [
              {
                "type": "command",
                "command": "node \"$CLAUDE_PROJECT_DIR/.claude/hooks/guard-tool.cjs\"",
                "timeout": 10
              }
            ]
          }
        ]
      },
      "cleanupPeriodDays": 90
    }
    
  • Headless-Gotcha: Im -p-Modus bricht ein nicht durch permissions.allow gedecktes Tool den Run ab (kein interaktiver Prompt). Das allow-Set muss die für das Profil nötigen Tools explizit enthalten — dokumentieren im README und in jedem Profile-Template.

2.6 Secrets/Env-Handling

Was: sanitizedEnv() Funktion, die Secret-artige Variablen entfernt, bevor der claude-Prozess gestartet wird. Whitelist statt Blacklist wo möglich.

Warum: Siehe §4.5 der Analyse — sonst kann jeder Agent per echo $DISCORD_BOT_TOKEN den Bot-Token exfiltrieren.

Wie:

  • Neue Datei: src/runtime/env.ts, exportiert sanitizedEnv(extra?: Record<string, string>).
  • Entfernt: DISCORD_BOT_TOKEN, DISCORD_CLIENT_SECRET, DISCORD_PUBLIC_KEY, alles matching /^(DISCLAW_SECRET_|SECRET_|TOKEN_|API_KEY_?)/i, plus eine konfigurierbare Block-Liste in disclaw.yaml.
  • Fügt hinzu: CI=true, DISCLAW_AGENT=1, DISCLAW_AGENT_NAME=<name>.

2.9 Workspace-Location: Standardmäßig außerhalb des DisClaw-Repos

Was: Workspaces liegen per Default unter ~/.disclaw/workspaces/<agent>/ (Linux/Mac) bzw. %USERPROFILE%\.disclaw\workspaces\<agent>\ (Windows). Der Pfad ist über disclaw.yamlworkspaces_root konfigurierbar, der Default ist aber immer absolut außerhalb des DisClaw-Repo-Roots. Das bisherige ./workspaces/ im Repo wird nicht mehr als Default verwendet (nur noch als Opt-in für Development/Tests — dann mit Warning-Log beim Bot-Start).

Warum: Siehe cli-feature-answers.md Frage 8. Claude Code walkt beim Start parent directories hoch und konkateniert alle gefundenen CLAUDE.md-Dateien in den System-Prompt. Liegt der Agent-Workspace unter C:\Code\side\disclaw\workspaces\<agent>\, dann sieht Claude zwangsläufig auch:

  1. C:\Code\side\disclaw\CLAUDE.md (DisClaw-Projekt-Identität)
  2. ~/.claude/CLAUDE.md (User-Scope, developer-spezifisch)
  3. ggf. weitere Parent-Scope-Dateien.

Das kontaminiert die Agent-Identität: Der Agent "weiß" plötzlich, dass er Teil von DisClaw ist, und erbt den Ton/Anweisungen der Developer-eigenen User-CLAUDE.md. Weder reproduzierbar noch sicher.

Zwei Verteidigungslinien, kombiniert:

  1. Strukturell: Workspaces liegen außerhalb des DisClaw-Repos. Dadurch existiert oberhalb keine Projekt-CLAUDE.md. ~/.claude/CLAUDE.md bleibt ein Rest-Leak-Vektor.
  2. Explizit: Alle CLI-Calls nutzen --bare (skippt CLAUDE.md-Walk-Up, Hooks-Discovery, Skills, Plugins, MCP, Auto-Memory komplett) und die Agent-Identität wird über --append-system-prompt-file <ws>/CLAUDE.md explizit injiziert. Das ist reproduzierbar und zukunftssicher — laut Doku wird --bare ohnehin der zukünftige Default für -p.

Konsequenz für bestehenden Code:

  • disclaw.yaml-Default: workspaces_root: "~/.disclaw/workspaces" (Tilde-Expansion + path.resolve gegen os.homedir()).
  • src/config/loader.ts: Tilde-Expansion implementieren, absolute Pfade per path.resolve normalisieren.
  • src/agent/runner.ts: Immer --bare + --append-system-prompt-file an den Call anhängen.
  • src/commands/new-agent.ts: Workspace unter dem neuen Root anlegen, Channel-Name und DB-Einträge identisch wie vorher.
  • Migrationshinweis für bestehende Workspaces: Ein /migrate-workspaces-Command (Phase 6+) oder manuelle Instruktion im README: Bestehende ./workspaces/<agent>/-Ordner nach ~/.disclaw/workspaces/<agent>/ verschieben, DB-Spalte workspace_path per UPDATE anpassen, Channel-Mapping bleibt. Im MVP reicht ein dokumentierter manueller Schritt.
  • Dev-Opt-in: Wenn workspaces_root innerhalb des DisClaw-Repos liegt (erkannt via path.relative(repoRoot, wsRoot) startet nicht mit ..), loggt der Bot beim Start eine Warnung und setzt zusätzlich claudeMdExcludes im gerenderten .claude/settings.json mit absoluten Pfaden für die DisClaw-Projekt-CLAUDE.md und alle .claude/rules/**-Globs des Parents.

Quelle / Status: Bestätigt via cli-feature-answers.md Frage 8 mit Zitat aus code.claude.com/docs/en/memory. Konfidenz: hoch.

2.7 Logging und Error-Handling

Was: pino als strukturierter JSON-Logger. Child-Logger pro Komponente (bot, router, runner, commands.new-agent). Error-Handler-Strategie: Fehler werden geloggt (mit Stack), der Discord-Channel bekommt eine sanitized Nachricht (keine absoluten Pfade, keine Stack Traces).

Warum: console.log ist bei mehreren Agenten und paralleler Ausführung unlesbar. Pino ist schnell, JSON-first, und Standard in Node-Server-Apps. Pretty-Printing nur in dev via pino-pretty.

Wie:

  • Neue Datei: src/runtime/logger.ts, exportiert rootLogger und childLogger(component: string).
  • Sanitizer: src/runtime/sanitize.ts, entfernt absolute Pfade (Regex auf ROOT_DIR), Tokens, Home-Pfad.

2.8 Test-Strategie: Vitest, Unit-first

Was: vitest als Test-Runner. Unit-Tests für reine Funktionen (splitForDiscord, sanitizedEnv, parseClaudeJsonOutput, agent-name-validation, channel-queue). Integration-Tests mit gemockten Discord- und CLI-Clients. Keine E2E-Tests im MVP (zu fragil gegen echte Discord-API).

Warum: Vitest hat minimale Config, läuft nativ mit TypeScript via esbuild, und ist für diese Projektgröße angemessener als Jest.

Wie:

  • tests/unit/ für reine Funktionen.
  • tests/integration/ für Router + Runner mit Mock-Spawner, Mock-DiscordChannel.
  • Mock-Strategie für claude CLI: Ein Fake-Spawner der ein Fake-Claude-Script aufruft (tests/fixtures/fake-claude.mjs), das deterministische JSON-Antworten ausgibt. So testet man die echten spawn-Pfade, ohne von einer echten Claude-Installation abhängig zu sein.
  • Discord: @discordjs/core-Mocks oder manuell via Interface-Abstraktion — siehe §5 "Discord-Adapter".

3. Tools und Libraries

Wird verwendet

Paket Version Zweck Begründung
discord.js ^14.16.0 Discord-Client Schon drin, de-facto Standard.
better-sqlite3 ^11.0.0 SQLite-DB Schon drin, synchrone API passt zum Handler-Modell.
dotenv ^16.4.0 .env-Loader Schon drin.
yaml ^2.6.0 YAML-Parser für disclaw.yaml und agent.yaml Schon drin.
zod ^3.23 Runtime-Validierung für Config, CLI-JSON-Output, Slash-Command-Args Sichert Grenzen zwischen untrusted Input und Code. Klein, ohne Runtime-Abhängigkeiten.
cross-spawn ^7 Plattformübergreifender child_process.spawn-Ersatz Löst Windows-.cmd-Wrapper korrekt auf (claude.cmd), quoted Windows-Args sicher, umgeht CVE-2024-27980 (Node 20.12+). Ohne Shell, also kein Command-Injection-Risiko. ~30 kB, keine transitiven Deps. Siehe §2.2.
@types/cross-spawn ^6 Typdefinitionen devDep.
pino ^9 Strukturiertes Logging Schnell, JSON-first, Child-Logger.
pino-pretty ^11 Dev-only Pretty-Printer Nur in devDependencies.
vitest ^2 Test-Runner Native TS, schnell, minimal Config.
@types/node ^22 Typdefinitionen Schon drin.
typescript ^5.7 Compiler Schon drin.

Wird explizit NICHT verwendet

  • Kein ORM (Prisma/Drizzle): Overkill für 2 Tabellen. better-sqlite3-Prepared-Statements sind schnell und typisiert genug via manuelle Interfaces.
  • Kein Express/Fastify: Kein HTTP-Endpoint im MVP. Wenn später ein Web-Dashboard dazu kommt, Fastify.
  • Kein Redis/BullMQ: Single-Process-Queue reicht. Externe Queues erst bei Multi-Instance-Deployment.
  • Kein discord-markdown o. ä.: Splitting ist simpel genug für Eigenbau und genauer kontrollierbar.
  • Kein Jest: Vitest ist leichter, schneller, direkt TS-fähig.
  • Kein @anthropic-ai/sdk oder claude-agent-sdk: ADR-001 sagt CLI-only.
  • Kein winston: Pino ist schneller und hat bessere Child-Logger-Semantik.

4. Phasenplan

Jede Phase ist in sich lauffähig. Abhängigkeiten sind explizit genannt. Nummerierung ist nicht chronologisch, sondern logisch.

Phase 0 — Härtung (P0, blockiert jede Live-Nutzung)

Ziele: Sicherheitslücken schließen, bevor irgendein echter Discord-Server angeschlossen wird.

Tasks:

  1. src/agent/runner.ts: shell: true entfernen; stattdessen cross-spawn verwenden (siehe §2.2). Neue Helper-Datei src/runtime/resolve-claude.ts: liest CLAUDE_PATH aus .env oder führt einmalig where claude/which claude aus, cached das Ergebnis in-memory, wirft eine klare Fehlermeldung bei Nichtfund.
  2. src/runtime/env.ts (neu): sanitizedEnv() wie in §2.6. In runner.ts statt ...process.env verwenden.
  3. Workspace-Root-Umstellung (§2.9): disclaw.yaml-Default auf ~/.disclaw/workspaces umstellen. src/config/loader.ts: Tilde-Expansion + path.resolve. src/commands/new-agent.ts: Workspace unter dem neuen Root anlegen. Warning-Log, wenn der konfigurierte Root innerhalb des DisClaw-Repos liegt. Manueller Migrationshinweis für bestehende ./workspaces/<agent>/-Ordner im README.
  4. src/commands/new-agent.ts: Name-Regex behalten, zusätzlich Path-Containment-Check:
    const root = path.resolve(config.workspaces_root);
    const wsPath = path.resolve(root, name);
    if (!wsPath.startsWith(root + path.sep)) throw new Error("Path traversal");
    
  5. src/agent/identity.ts: .claude/settings.json-Template härten — Schema wie in §2.5 (mit permissions.allow/ask/deny/defaultMode und dreistufigem hooks-Block). Deny für ../**, Deny-Patterns für gefährliche Bash-Kommandos.
  6. src/agent/identity.ts: CLAUDE.md-Template bekommt die Prompt-Injection-Resistenz-Klausel (Punkt 5 aus §3 der Analyse).
  7. src/agent/runner.ts: CLI-Call immer mit --bare --append-system-prompt-file <ws>/CLAUDE.md aufbauen (siehe §2.9), damit Parent-CLAUDE.md-Leaks strukturell ausgeschlossen sind — auch für Dev-Mode-Workspaces innerhalb des Repos.
  8. tests/unit/sanitize-env.test.ts, tests/unit/path-traversal.test.ts, tests/unit/resolve-claude.test.ts (mocked), tests/unit/workspace-root-resolve.test.ts (Tilde-Expansion, Warning bei Repo-internem Pfad).

Definition of Done:

  • Kein shell: true mehr im Code; Spawning läuft über cross-spawn mit resolvtem Claude-Pfad.
  • Ein Test demonstriert, dass DISCORD_BOT_TOKEN nicht im Child-Env landet.
  • Ein Test demonstriert, dass name: "../etc" als Slash-Command-Input abgewiesen wird.
  • .claude/settings.json in einem frisch erstellten Workspace enthält die Deny-Liste und das korrekte hooks.PreToolUse[].matcher-Layout.
  • CLAUDE.md enthält den Prompt-Injection-Resistenz-Satz.
  • Default-workspaces_root zeigt auf ~/.disclaw/workspaces, Tilde wird korrekt expandiert, Warning bei repo-internem Pfad.
  • CLI-Calls enthalten --bare und --append-system-prompt-file <ws>/CLAUDE.md; ein Test (mit Fake-Claude) verifiziert, dass kein Parent-CLAUDE.md-Leak auftritt.

Abhängigkeiten: Keine. Erste Phase.

Phase 1 — Solides MVP

Ziele: Ein Agent antwortet zuverlässig auf Discord, mehrfache Messages im selben Channel werden serialisiert, Fehler sind verständlich, Logs sind strukturiert, Grundstruktur ist testbar.

Tasks:

  1. src/runtime/logger.ts (neu): Pino-Setup, rootLogger, childLogger(component). In index.ts, bot.ts, router.ts, runner.ts, commands/new-agent.ts console.* ersetzen.
  2. src/runtime/sanitize.ts (neu): sanitizeForDiscord(text, rootDir) — entfernt Abspfade.
  3. src/runtime/channel-queue.ts (neu): ChannelQueue-Klasse wie in §2.3. In router.ts integrieren.
  4. src/runtime/concurrency.ts (neu): Semaphore(max: number)-Klasse. Globaler Cap aus disclaw.yamlmax_concurrent_agents: 4.
  5. src/discord/split.ts (neu): splitForDiscord() wie in §2.4. router.ts verwendet ihn.
  6. src/agent/runner.ts: API-Refactoring auf RunResult = { text: string, sessionId?: string, toolCalls?: ToolCall[] }. Return-Type vorbereiten für spätere Streaming-Unterstützung (runAgent() gibt heute Promise<RunResult>, später zusätzlich runAgentStream()).
  7. src/runtime/zod-schemas.ts (neu): Zod-Schemas für disclaw.yaml, agent.yaml, Claude-CLI-JSON-Output. parseClaudeJsonOutput() validiert via Zod.
  8. src/config/loader.ts: Validierung über Zod statt as Cast.
  9. src/db/database.ts: Schema-Inline-String entfernen, stattdessen src/db/schema.sql laden (Single Source of Truth).
  10. tests/unit/split-for-discord.test.ts (Fences, lange Zeilen, Fallback), tests/unit/channel-queue.test.ts, tests/unit/parse-claude-output.test.ts.
  11. tests/integration/router.test.ts: mockt DB, DiscordChannel (Interface), Spawner — verifiziert das End-to-End-Verhalten bei parallelen Nachrichten (Reihenfolge, keine Race).
  12. .env.example hinzufügen.
  13. package.json: test-Script, devDependencies für vitest, pino-pretty, zod, pino.
  14. docs/cli-feature-probe.md (neu): Manuelles Regression-Script, das die kritischen Annahmen aus cli-feature-answers.md (Session-ID top-level, usage-Block-Pfade, stream-json-Event-Typen, Session-Slug-Regel) gegen die lokal installierte CLI verifiziert und Resultate dokumentiert. Läuft als Sanity-Check bei CLI-Upgrades.

Definition of Done:

  • Bot startet, erstellt Management-Channel, akzeptiert /new-agent, antwortet im Agent-Channel korrekt auf eine Nachricht.
  • Drei schnell hintereinander gesendete Nachrichten im gleichen Channel werden in exakter Reihenfolge verarbeitet (verifiziert im Integration-Test).
  • Log-Output ist strukturiertes JSON (mit pino-pretty in dev).
  • npm test läuft grün, coverage für split, channel-queue, sanitize-env, parse-claude-output, router.
  • Kein console.log mehr im Produktionscode (nur in Tests/Probe-Scripts erlaubt).

Abhängigkeiten: Phase 0.

Phase 2 — Context-Effizienz (--resume) — FREIGEGEBEN

Ziele: Prompt-Cache-Hits, drastisch gesenkte Input-Tokens, saubere Historie-Semantik.

Status: Freigegeben durch cli-feature-answers.md Frage 1+2. -p + --resume ist dokumentiert, session_id liegt top-level im JSON-Output, Session-ID bleibt beim Resume stabil (es sei denn --fork-session).

Tasks:

  1. DB-Migration: ALTER TABLE workspaces ADD COLUMN claude_session_id TEXT; ALTER TABLE workspaces ADD COLUMN session_updated_at TEXT;. In schema.sql pflegen, in database.ts als Migration-Step addieren (simpler try { ALTER } catch {}-Block reicht).
  2. src/db/database.ts: Neue Methoden setSession(workspaceId, sessionId), getSession(workspaceId), clearSession(workspaceId). setSession aktualisiert auch session_updated_at mit aktuellem ISO-Timestamp.
  3. src/agent/runner.ts: Session-Flow wie in §2.1. Nach jedem erfolgreichen Run store.set(ws.id, json.session_id) aufrufen — auch wenn die ID gleich geblieben ist (defensive Update-after-run). Fallback-Pfad implementieren: Bei --resume-Fehler store.clear + Retry ohne --resume (optional --continue als Zwischenstufe).
  4. Die History-Freitext-Injektion nur noch als Cold-Start-Rehydrierung verwenden (strukturierter als heute, mit klaren <turn role="user|assistant"> Blocks).
  5. tests/integration/session-resume.test.ts: Fake-Claude gibt beim ersten Call eine session_id top-level zurück, der zweite Call muss diese via --resume <id> wieder verwenden. Fehlschlag simulieren → Fallback-Pfad wird getroffen. Edge-Case: Fake-Claude gibt beim zweiten Call eine neue session_id zurück (Fork-Szenario) → DB muss auf neue ID updaten.
  6. Metrik-Logging: usage-Block aus JSON-Output parsen. Die exakten Pfade (result.usage.cache_read_input_tokens vs. top-level usage.cache_read_input_tokens) werden im Probe-Script empirisch verifiziert und dann hartcodiert. Log-Line: log.info({ input_tokens, cache_read, cache_create, output_tokens }, "claude_run").
  7. Optional (Nice-to-have): Neue Tabelle agent_runs(workspace_id, ts, input_tokens, cache_read, cache_create, output_tokens, duration_ms) für Pro-Agent-Metriken.

Definition of Done:

  • Zweite Nachricht im gleichen Channel verwendet dieselbe session_id (verifiziert via Fake-Claude).
  • Session-ID überlebt Bot-Neustart.
  • Bei bewusstem Löschen der Session-Datei reagiert der Runner mit automatischem Fallback, speichert neue Session-ID, keine User-sichtbare Fehlermeldung.
  • session_id wird nach jedem Run in der DB geschrieben, auch wenn identisch.
  • Log-Line zeigt gecachte Tokens.

Abhängigkeiten: Phase 0, Phase 1.

Phase 3 — UX-Feinschliff

Ziele: Discord-Ausgabe fühlt sich gut an. Lange Antworten, Codeblöcke, Attachments, Typing-Refresh.

Tasks:

  1. src/router.ts: Typing-Refresh-Intervall auf 8 s. Typing wird nur so lange ausgesendet, wie der runAgent-Call läuft.
  2. src/discord/send-response.ts (neu): sendResponse(channel, text, { asAttachmentAfter = 8000 }). Wenn Text > Schwelle → als .md-Attachment senden.
  3. Attachment-Support eingehend: router.ts prüft message.attachments. Download in workspaces/<agent>/.disclaw-inbox/<ts>-<name>.ext. Im Prompt referenziert. Nach erfolgreicher Antwort: TTL-Cleanup-Task.
  4. Input-Limit: User-Nachricht > 4000 Zeichen → Reaction ⚠️ und Hinweis im Channel.
  5. Reaction 👀 sobald Message in Queue landet; bei Erfolg; bei Fehler. Alle .catch(() => {}), nicht blockierend.
  6. tests/unit/send-response.test.ts, tests/unit/attachment-inbox.test.ts (mit tmp-Dir).

Definition of Done:

  • Lange Antworten werden korrekt gesplittet ohne kaputte Fences.
  • Sehr lange Antworten werden als .md-File gesendet.
  • Bilder aus Discord landen im Workspace und Claude kann sie via Read-Tool sehen.
  • Typing-Indikator bleibt die ganze Laufzeit sichtbar, ohne "pausen".

Abhängigkeiten: Phase 1.

Phase 4 — Skills und Permissions (Profile)

Ziele: Agents sind pro Rolle sinnvoll voreingestellt. Hook-Guard ist aktiv. Audit-Log läuft.

Tasks:

  1. src/agent/profiles/ (neu): Ordner mit researcher.md, developer.md, writer.md, ops.md, sandboxed.md als CLAUDE.md-Templates und *.settings.json als .claude/settings.json-Templates. Platzhalter {{name}}, {{role}}, {{workspace_abs}}.
  2. src/agent/profiles/index.ts: ProfileRegistry — maps name → { claudeMd: Template, settings: Template, hooks: Template[] }.
  3. src/commands/new-agent.ts: Neuer Option-Parameter profile mit Choices. Default = developer.
  4. src/agent/identity.ts: setupAgentWorkspace() nimmt profile und rendert Templates aus der Registry.
  5. src/agent/hooks/guard-tool.cjs (generiert beim Create): Node-Script, das als PreToolUse-Hook jeden Tool-Call gegen den Workspace-Pfad prüft, außerhalb → exit 2 mit Block-Reason. Jeder Call wird zusätzlich in workspaces/<agent>/.disclaw-audit.jsonl geschrieben. Event-Typen gemäß cli-feature-answers.md Frage 3: PreToolUse, PostToolUse, UserPromptSubmit, SessionStart, Stop. Wir nutzen primär PreToolUse mit matcher: "Bash|Read|Write|Edit".
  6. .claude/settings.json-Template referenziert den Hook im dreistufigen Layout (hooks.PreToolUse[].hooks[].{type, command, timeout}), siehe §2.5.
  7. tests/unit/profile-registry.test.ts, tests/integration/hook-guard.test.ts (mit Fake-Claude, der einen bösartigen Tool-Call simuliert).

Definition of Done:

  • /new-agent name:r profile:researcher erstellt einen Agenten mit read-only-Permissions.
  • Ein Hook-Test verifiziert, dass Bash(cat ../../other/secret.txt) abgewiesen und im Audit-Log protokolliert wird.
  • Alle fünf Profile sind vorhanden und passen zu ihrer §3-Tabelle der Analyse.

Abhängigkeiten: Phase 0 (Settings-Template). Settings-Schema ist durch cli-feature-answers.md Frage 3 verifiziert.

Phase 5 — Streaming

Ziele: Antworten erscheinen inkrementell im Discord-Channel.

Tasks:

  1. src/agent/runner.ts: Zweiter Export runAgentStream(): AsyncIterable<StreamEvent>. Intern: claude -p "<msg>" --output-format stream-json --verbose --include-partial-messages --bare --append-system-prompt-file <ws>/CLAUDE.md (plus --resume wie in Phase 2). Die Flags --verbose und --include-partial-messages sind laut cli-feature-answers.md Frage 4 zwingend, sonst emittiert die CLI nur das Final-result-Event ohne Text-Deltas.
  2. NDJSON-Parser: Zeilenweise JSON.parse. Dispatch nach .type: system (init/api_retry, skip), assistant (Text- und Tool-Use-Blocks im Native-API-Format), user (Tool-Results), stream_event (partial deltas — interessant ist .event.delta.type === "text_delta" mit .event.delta.text), result (Final-Event, enthält session_id und usage). Parser defensiv gegen unbekannte Typen (skip statt crash) — CLI-Updates können neue Subtypes einführen.
  3. src/discord/stream-to-discord.ts (neu): Nimmt einen AsyncIterable<StreamEvent>, akkumuliert Text-Deltas, editiert die Bot-Message alle ~1200 ms (Discord-Rate-Limit), rolled auf neue Message bei >1900 Zeichen. Bei result-Event: finalen Stand commitet.
  4. router.ts: Feature-Flag streaming: true/false aus disclaw.yaml. Fallback auf Bulk-Mode bei Fehlern.
  5. tests/integration/streaming.test.ts mit Fake-Claude, der ein NDJSON-Scripted-Log auf stdout schreibt (system → assistant → mehrere stream_events mit text_deltas → result).

Definition of Done:

  • Streaming ist per Flag aktivierbar und funktioniert End-to-End.
  • Edits respektieren Discord-Rate-Limits (min. 1200 ms zwischen Edits).
  • Bei Fehler im Stream wird die Message sauber finalisiert.

Abhängigkeiten: Phase 1 (Runner-API vorbereitet). Phase 2 (Session-Resume bleibt auch im Stream-Modus aktiv).

Phase 6+ — Zukunft

Klar: nicht jetzt planen, aber Pfade offenhalten.

  • /list-agents, /delete-agent, /agent-config: Command-Handler-Interface aus §5 macht das zu einer 30-Minuten-Sache pro Command. /delete-agent muss Channel + Workspace + DB-Einträge + ~/.claude/projects/<hash>/-Session atomar löschen.
  • User-Ratelimit: Token-Bucket in src/runtime/ratelimit.ts, Map<userId, Bucket>.
  • Daily-Budget pro Agent: DB-Tabelle agent_usage(workspace_id, day, runs), Check vor jedem Run.
  • Agent-zu-Agent: Ein Agent kann eine Nachricht an einen anderen Agent-Channel adressieren → Router erkennt und delegiert.
  • Docker-Isolation: DockerRunner als zweite Implementierung des AgentEngine-Interface (§5).
  • Web-Dashboard: Fastify-Server, der DisclawDatabase teilt.
  • Alternative AI-Engine: Ein ClaudeSdkEngine gegen claude-agent-sdk für Nutzer ohne Claude-Pro-Abo. Gleiches AgentEngine-Interface.

5. Erweiterbarkeit (Fokus!)

Der Code ist so zu strukturieren, dass neue Features ohne Core-Änderungen andocken. Kernmechanismen:

5.1 Schichtentrennung

src/
  bot.ts               Discord-Adapter (Event → Router)
  router.ts            Routing + Queue + Sanitize (kennt Runner abstrakt)
  discord/             Alles Discord-nahe: split, send, stream-to-discord
  agent/
    engine.ts          AgentEngine-Interface (siehe 5.6)
    runner.ts          ClaudeCliEngine: implements AgentEngine
    identity.ts        Workspace-Create (delegiert an Profile)
    profiles/          Templates pro Profil
    hooks/             Pre/Post-Hook-Scripts
  commands/
    index.ts           CommandRegistry (siehe 5.2)
    new-agent.ts
    list-agents.ts     (später)
    delete-agent.ts    (später)
  runtime/
    channel-queue.ts
    concurrency.ts
    env.ts
    logger.ts
    sanitize.ts
    ratelimit.ts       (später)
  db/
    database.ts        Repository-Stil, keine Business-Logik
    schema.sql
  config/
    loader.ts

Regel: runtime/, db/, discord/ sind Leaf-Module, kennen sich nicht gegenseitig über die Grenzen hinweg. agent/ kennt runtime/ und db/-Typen, aber nicht discord.js. router.ts ist der einzige Ort, der Discord und Agent zusammenbringt.

5.2 Plugin-Interface für Slash-Commands

Neues Interface, das neue Commands an einer Stelle registriert:

// src/commands/types.ts
import { SlashCommandBuilder, ChatInputCommandInteraction } from "discord.js";
import { DisclawDatabase } from "../db/database";
import { DisclawConfig } from "../config/loader";

export interface CommandContext {
  db: DisclawDatabase;
  config: DisclawConfig;
  managementChannelId: string | null;
}

export interface DisclawCommand {
  definition: SlashCommandBuilder;
  handle(interaction: ChatInputCommandInteraction, ctx: CommandContext): Promise<void>;
}
// src/commands/index.ts
import { newAgentCommand } from "./new-agent";
// import { listAgentsCommand } from "./list-agents";
export const commands: DisclawCommand[] = [
  newAgentCommand,
  // listAgentsCommand,
];

bot.ts registriert alle Commands in einer Schleife und dispatched via commandName-Lookup. Neuer Command = neue Datei + ein Array-Eintrag. Keine Änderung an bot.ts nötig.

5.3 Profile-Templates als Registry

// src/agent/profiles/index.ts
export interface ProfileTemplate {
  name: string;
  description: string;
  renderClaudeMd(ctx: { name: string; role: string }): string;
  renderSettings(ctx: { workspaceAbs: string; name: string }): Record<string, unknown>;
  hooks?: HookDefinition[];
}

export const profiles: Record<string, ProfileTemplate> = {
  developer: developerProfile,
  researcher: researcherProfile,
  writer: writerProfile,
  ops: opsProfile,
  sandboxed: sandboxedProfile,
};

Ein neuer Profil-Typ (z. B. data-scientist) ist eine neue Datei + ein Registry-Eintrag. identity.ts muss nicht angefasst werden.

5.4 Hook-System (Pre/Post Message, Pre/Post Spawn)

// src/agent/hooks.ts
export type HookEvent =
  | { type: "preMessage"; workspace: Workspace; message: string }
  | { type: "postMessage"; workspace: Workspace; response: string }
  | { type: "preSpawn"; workspace: Workspace; args: string[] }
  | { type: "postSpawn"; workspace: Workspace; result: RunResult };

export type Hook = (event: HookEvent) => Promise<void | { cancel?: boolean; reason?: string }>;

export class HookBus {
  private hooks: Hook[] = [];
  register(h: Hook) { this.hooks.push(h); }
  async emit(e: HookEvent): Promise<{ cancel: boolean; reason?: string }> { ... }
}

router.ts und runner.ts emittieren an einen global injizierten HookBus. Beispiel-Hooks: Audit-Logger, Ratelimit, Daily-Budget, Metriken. Alle außerhalb des Core-Codes, einfach pluggable.

Wichtig: Die .claude/-Hooks (PreToolUse etc.) sind ein anderer Mechanismus — die werden von Claude Code selbst gefeuert. Das HookBus hier ist DisClaw-intern.

5.5 MCP-Server pro Workspace (vorbereitet, später aktiviert)

Das Profile-Template kann optional eine mcpServers-Sektion in die .claude/settings.json rendern. Die Liste der verfügbaren MCPs ist in src/agent/mcp/registry.ts zentral gepflegt. Aktivierung pro Profil deklarativ:

developerProfile.mcpServers = ["git"];
opsProfile.mcpServers = ["docker", "k8s"];

Der Renderer fügt die korrekten Stanzas in settings.json ein. Kein Code-Change im Runner nötig.

5.6 Abstraktion der AI-Engine

// src/agent/engine.ts
export interface AgentEngine {
  run(opts: RunOptions): Promise<RunResult>;
  runStream?(opts: RunOptions): AsyncIterable<StreamEvent>;
}

ClaudeCliEngine ist heute die einzige Implementierung. Später könnten ClaudeSdkEngine (direkte API) oder DockerClaudeCliEngine (CLI in Container) als Drop-in-Alternativen dazu kommen. Der Router kennt nur das Interface.

5.7 Discord-Adapter-Abstraktion (für Testbarkeit)

Alle Discord-Interaktionen im Router gehen über ein kleines Interface ChannelSink:

export interface ChannelSink {
  send(text: string): Promise<{ id: string }>;
  edit(messageId: string, text: string): Promise<void>;
  sendTyping(): Promise<void>;
  sendFile(name: string, content: Buffer): Promise<void>;
  react(messageId: string, emoji: string): Promise<void>;
}

In Produktion wraps das einen TextChannel. In Tests ist es ein Mock. So sind Router-Tests komplett ohne discord.js möglich.


6. Offene Fragen an den AI Engineer

Die ursprünglichen 10 Fragen wurden am 2026-04-08 vom AI Engineer beantwortet. Siehe docs/cli-feature-answers.md für Volltext, Quellen und Konfidenzangaben.

Verbleibende Rest-Unsicherheiten (alle empirisch via Probe-Script zu bestätigen, kein Blocker für Phase-Start):

  1. Exakte JSON-Pfade des usage-Blocks (result.usage.cache_read_input_tokens vs. top-level usage.cache_read_input_tokens): Die Existenz der Felder ist durch das API-Layer garantiert, aber der Output-Shape der CLI wird in Phase 2 per Probe-Script empirisch verifiziert und dann im Parser hartcodiert.

  2. stream-json Event-Schema-Stabilität: Die groben Typen (system, assistant, user, stream_event, result) sind dokumentiert, aber Subtypes und Feld-Layouts können zwischen CLI-Versionen driften. Mitigation: Parser defensiv (skip unknown), Regression-Test im Probe-Script.

  3. Session-Slug-Transformation auf Linux/Mac: Windows-Slug-Regel (C:\Code\side\disclawC--Code-side-disclaw) ist empirisch bestätigt, Linux-Regel (/home/nick/...-home-nick-...) ist aus Analogie abgeleitet. Muss auf Linux-Runner geprüft werden, sobald /delete-agent implementiert wird (Phase 6+).

  4. Multimodal-Passthrough ohne Tool-Round-Trip: @./path/image.png im Prompt funktioniert via Read-Tool + Vision; ein direkter Vision-Input-Block ohne Round-Trip existiert im CLI--p-Modus nicht und würde eine Migration zum Agent SDK erfordern. Scope-Entscheidung: bleibt Phase 6+.


7. Risiken und Mitigation

Risiko Wahrscheinlichkeit Impact Mitigation
--resume funktioniert nicht wie erwartet in der installierten CLI Niedrig Hoch (Phase 2 blockiert) Per cli-feature-answers.md Frage 1+2 offiziell dokumentiert und bestätigt. Probe-Script als Regression-Check bleibt. Fallback-Pfad (DB-Rehydrierung + --continue) bleibt als Degradation verfügbar.
CLAUDE.md-Walk-Up kontaminiert Agenten-Identität (Parent-Projekt-CLAUDE.md oder ~/.claude/CLAUDE.md leaked in System-Prompt) Mittel (strukturell gegeben, wenn nicht mitigiert) Hoch (Reproduzierbarkeit, Sicherheit, Identitäts-Leak) §2.9: Workspaces außerhalb des DisClaw-Repos (~/.disclaw/workspaces/) plus --bare + --append-system-prompt-file bei jedem Call. Integration-Test mit Fake-Claude, der den empfangenen System-Prompt echoed, und Assertion, dass ausschließlich die Workspace-CLAUDE.md drin ist. Für Dev-Mode-Workspaces innerhalb des Repos zusätzlich claudeMdExcludes im Settings.
Windows-Spawn schlägt auf frischer Node-20.12+-Installation fehl (claude.cmd, CVE-2024-27980) Mittel (default), niedrig (nach Phase 0) Hoch (DisClaw startet auf Windows gar nicht) §2.2: cross-spawn als Dependency, where claude-Lookup mit absolutem Pfad-Cache, klare Fehlermeldung wenn nicht auffindbar. CI-Test auf Windows-Runner (GitHub Actions windows-latest) verifiziert Smoke-Test-Spawn.
Command Injection bleibt unentdeckt in einem Codepfad Niedrig (nach Phase 0) Kritisch shell: false als Lint-Regel im CI. Code-Review-Checkliste. Integration-Test mit bösartigem Input.
Prompt-Injection bricht Permissions aus Mittel Hoch Defense in depth: CLAUDE.md-Klausel, settings.json deny, PreToolUse-Hook, Audit-Log. Keine davon allein reicht.
.claude/settings.json-Schema ändert sich zwischen CLI-Versionen Mittel Mittel Schema-Version beim Workspace-Create detektieren (claude --version parsen), Template pro Release pflegen. Warnung bei unbekannter Version.
Parallele Nachrichten im selben Channel korrumpieren Workspace-Dateien Hoch (ohne Queue) Hoch Per-Channel-Queue in Phase 1. Integration-Test.
Secret-Exfiltration via echo $DISCORD_BOT_TOKEN Hoch (aktuell) Kritisch sanitizedEnv() in Phase 0. Test, der den resultierenden Env-Set prüft.
Discord-Rate-Limits bei Streaming Niedrig Mittel Min. 1200 ms zwischen Edits, neue Message bei Überlauf. discord.js 429-Handling nutzen.
Speicher-Leaks bei Long-Running-Bot-Prozess Niedrig Mittel Pino-Logs + process.memoryUsage() alle 5 min als Debug-Log. channelQueues-Map räumt sich selbst auf.
Zwei DisClaw-Instanzen gegen dieselbe DB/Workspace Niedrig Hoch Lock-File im Workspaces-Root beim Start. Fehlschlag, wenn Lock existiert und PID noch läuft.
Benutzer editieren CLAUDE.md manuell mit Injection Mittel Niedrig (der Benutzer "schadet" sich selbst) Dokumentiert, kein technischer Fix.
PreToolUse-Hook-Script ist fehlerhaft und blockt legitime Calls Mittel Mittel Hook schreibt Audit-Log auch bei Block; /reset-permissions-Command später; im MVP manuelles Editieren der Settings.
Test-Suite wird träge, wenn echte claude CLI involviert ist Mittel Niedrig Unit-Tests nutzen Fake-Spawner. Echte-CLI-Tests nur in tests/probe/ und manuell.