- Session manager usage: session-management.md (session management guide)
- Handoff agent usage: handoff.md (automatic memory docs guide)
- Search usage: search.md (transcript search guide)
- Skills usage: skills.md (review, understand, panel guide)
- Visual diagrams: diagrams.md (architecture diagrams)
- Reference details: design_appendix.md (schemas, config tables, implementation specifics)
Forge is not a monolith. It is the connective tissue between specialized tools -- a monorepo of proven tools
sharing common libraries (Auth, Models, State) under a unified interface (forge CLI).
These components run independently but share code (libraries/config).
| Component | Responsibility | Location |
|---|---|---|
| Forge Proxy | Model routing, Auth, Tool fixing | src/forge/proxy/ |
| Forge Session | Session isolation, Worktrees | src/forge/session/ |
| Forge Skills | Agent workflows (Review, Planning) | src/skills/ + forge CLI |
| Forge Status | Visual feedback & Dashboard | src/forge/status/ |
| Forge Policy | Policy enforcement (TDD, safety) | src/forge/guard/ |
| Commands/Agents | Claude Code extensions | src/{commands,agents}/ |
| Hooks | Lifecycle events (Claude Code) | src/forge/cli/hooks/ |
See diagrams.md §1: Core Architecture Overview for a visual overview.
Forge uses file-based state instead of a DB. Two concepts are first-class and must not be conflated:
- Session: a Claude coding session (worktree, artifacts, user intent, hook-confirmed facts)
- Proxy: a proxy endpoint identity (base URL / port / template) that the proxy can actually enforce
Why proxy instances? Claude Code proxy requests do not include a session identifier, so the proxy cannot know which session made a request. The only way to apply different routing or hyperparameters is to run separate proxy instances on different ports. A proxy instance is one such endpoint (base_url + port + template). Sessions reference proxies but cannot change proxy-owned routing—this is a technical constraint, not a design choice.
See diagrams.md §2: Session vs Proxy Separation for a visual explanation.
The Proxy Orchestrator lives in the Forge CLI (forge proxy subcommands). It manages proxy lifecycle: start
instances, register them in the proxy registry, and clean up stale proxies.
Forge uses a three-part contract:
- Session file (per Forge project):
<forge_root>/.forge/sessions/<session_name>/forge.session.json - Proxy registry (global):
~/.forge/proxies/index.json→ running proxies (template ↔ base_url ↔ pid) - Runtime truth (proxy mode only): live proxy introspection (
GET /at the proxy base URL)
Clarification: The session file is for session UX (artifacts, status,
forge sessioncommands), not proxy routing. The proxy's routing identity is the proxy base URL only.Parallel sessions: Multiple sessions can run in the same Forge project. Each session has its own subdirectory under
.forge/sessions/. Hooks identify the session viaFORGE_SESSIONset at launch.
Forge has four scoping levels. They must be explicitly defined to avoid path confusion:
project_root (logical repo -- git identity, shared across worktrees)
+-- checkout_root (this worktree -- git rev-parse --show-toplevel)
+-- forge_root (enabled .claude/ + .forge/ project inside the checkout)
+-- working_dir (launch CWD -- for managed sessions, equals forge_root)
| Level | Identity source | Stored as | Purpose |
|---|---|---|---|
| Logical Repo | get_main_repo_root() (git) |
project_root |
Cross-project ops, session list default scope |
| Checkout | git rev-parse --show-toplevel |
checkout_root |
Worktree targeting for --into, relative_path anchor |
| Forge Project | Directory with .claude/ + .forge/ |
forge_root |
Session root, artifact root, state scoping anchor |
| Working Dir | Launch CWD (= forge_root for managed sessions) |
implicit | Managed sessions always launch from forge_root |
Four foundational rules (normative):
- A session may start only where
forge extension enablehas established a project/local install (.forge/exists). - The session root is exactly that install root (the Forge project root,
forge_root). - Session state is scoped to
forge_root-- manifests, artifacts, search index,prev_sessions/all live under that.forge/. - Project/local
forge extension enablerequires.claude/at the target directory. If missing, it is created silently (it is a directory, not a config file -- no ambiguity, no interactive prompt needed). User-level install (--user) goes to~/.claude/and does not require a project anchor.
Definitions:
- Forge project = directory containing both
.claude/and.forge/, established byforge extension enable. forge_root= the Forge project root (where.forge/lives). Field inSessionIndexEntry.relative_path=forge_rootrelative tocheckout_root. Preserved onfork --into.
Fork --into rules (normative):
--intotargets a worktree (different checkout), not an arbitrary path.- Child session lands at the equivalent
forge_rootin the target worktree:target_checkout_root / relative_path. - Target must have Forge enabled at that relative path. If not: error with "Run
forge extension enablein<target_checkout_root>/<relative_path>first, or use--worktreeto create a new checkout with auto-enable." - No arbitrary path targeting -- you pick the worktree, the position is computed.
Session command scoping (normative):
session list: repo-scoped by default (filter byproject_root). Shows sessions across all Forge projects within the logical repo.--scope projectnarrows to currentforge_root.--scope allshows everything globally.session show,session delete(named),session set,session reset: repo-scoped with current-project preference. Two-tier resolution: try currentforge_rootfirst (O(1)), fall back to repo-scoped scan. Prefers currentforge_rootas tiebreaker when the same name exists in multiple projects. RaisesAmbiguousSessionErrorif truly ambiguous. Prints a cross-project note when resolving from a differentforge_root.session delete --all: project-scoped (currentforge_rootonly). Requires being inside a Forge project (_cwd_forge_root() != None); refuses to run outside one to prevent accidental global deletion.session resume,session fork: project-scoped. Cannot resolve cross-project because Claude Code's--resumeand CWD namespace are tied to the project directory. Hints where the session lives on cross-project miss.session clean: global by default (noforge_rootfilter).- Artifacts, handoff, search: Forge-project-scoped (all under
<forge_root>/.forge/). - Cross-project resume (handoff mode only): allowed within the same logical repo
(
parent_project_root == child.project_root). Reads parent artifacts by absolute path viaparent_forge_rootin the derivation record. Native resume (--resume-mode native) requires the sameforge_root-- Claude Code cannot--resumeacross CWD boundaries (see §3.9).
Exception: forge claude start (bare launcher) works without .forge/. It does not create session state, does not
set FORGE_SESSION, and session-specific hooks/status behavior is a no-op. See §3.4.
See diagrams.md §10: Project Identity Hierarchy for a visual overview.
Claude Code scopes conversations to the project directory (.claude/). --resume <uuid> only finds conversations in
the current project's .claude/. Forge's project model (N sessions per Forge project, cross-project forking) extends
this.
When sessions cross Forge project boundaries (worktree forks, fork --into, resume), Forge uses file-based
handoff: process_handoff() reads the parent's transcript artifacts and generates a portable context file at
<forge_root>/.forge/prev_sessions/<parent>.md, appended at launch via --append-system-prompt-file. This is an
accepted tradeoff: handoff files are lossy compared to native --resume (structured summary vs full conversation), but
they enable branch isolation and cross-worktree workflows.
The --strategy knob controls fidelity: minimal (lineage pointer) → structured (conversation skeleton, default) →
full (complete transcript) → ai-curated (LLM-selected highlights). --inline-plan embeds the approved plan content
(from ExitPlanMode snapshots) directly into the handoff file — critical for review and supervision workflows where the
reader cannot access the original plan file.
Checkouts are shared resources (like proxies): multiple sessions can live in the same checkout. delete_session()
scans for co-resident sessions before removing a worktree, and sessions created via --into (owns_worktree=False)
never remove the worktree they're visiting. If the owning session is deleted before the last guest, Forge preserves the
checkout and leaves final cleanup to the user.
This workflow motivates Forge's separation of Session and Proxy.
Goal: Combine meticulous planning/review from one proxy (e.g., OpenAI-based) with fast/high-quality implementation from another, while keeping artifacts and the working directory shared.
Baseline flow: Session A (planner, OpenAI proxy) → fork to Session B (executor, Anthropic proxy) → review loop (resume A to review B's changes, feed fixes back). Optional Session C on a third proxy for independent review/synthesis.
Why proxies, not session overrides: Per-session routing is impossible without a session identifier in requests (see §3). Sessions within a Forge project share the working directory; artifacts (plans, reviews) are captured per-session for cross-session handoff. Worktrees are used when sessions write concurrently.
| Artifact | Path | Owned by | Purpose |
|---|---|---|---|
| Session file | <forge_root>/.forge/sessions/<session_name>/forge.session.json |
Forge Session + Hooks | Session intent, overrides, hook-written confirmed |
| Global session index | ~/.forge/sessions/index.json |
Forge Session | Session metadata (name, forge_root, project_root); fast listing + project filtering |
| Active session index | ~/.forge/sessions/active.json |
Forge Session | Ephemeral live-launch registry for delete warnings + stale pruning |
| Proxy registry | ~/.forge/proxies/index.json |
Forge Proxy Orchestrator | Running proxies (template ↔ base_url/port ↔ pid) |
| Runtime config | ~/.forge/config.yaml |
Forge CLI | Global runtime preferences (proxy mode, timeouts, context limit) |
| Installed manifest | ~/.forge/installed.json |
Forge Installer | Tracks what forge extension enable installed for update/uninstall |
| Work queue | ~/.forge/pending-work/*.json |
Forge Work Queue (§3.13) | Deferred work markers (stop, index, handoff) |
| Optional events | ~/.forge/events/*.jsonl |
TBD | Debugging/analytics; optional |
The active session index is intentionally runtime-only. It is self-healed via launcher PID / sidecar container liveness checks and must not be treated as durable session truth like the manifest or global session index.
Global session index entry schema (~/.forge/sessions/index.json):
@dataclass
class SessionIndexEntry:
project_root: str # Logical repo -- cross-project ops, session list default scope
checkout_root: str # Worktree root -- --into targeting, relative_path anchor
forge_root: str # Forge project root -- state scoping anchor
relative_path: str # forge_root relative to checkout_root
last_accessed_at: str
is_fork: bool = False
is_incognito: bool = False
parent_session: str | None = None
claude_session_id: str | None = Nonesession list --scope controls filtering: repo (default) filters by project_root -- shows sessions across all
worktrees and Forge projects within the logical repo. project filters by forge_root -- just this Forge project.
all shows everything globally.
1:1 invariant: Each Forge session corresponds to exactly one Claude process invocation.
confirmed.claude_session_id is launch-owned — it starts as None when a session is created, and is set by the
SessionStart hook when Claude actually starts. A non-null claude_session_id means "this session has been used."
Relaunching a used session creates a child with lineage (parent_session), not a reuse of the same session.
Default resume behavior. forge session resume <name> reattaches to the same Claude conversation without creating a
child. This relaxes the 1:1 model (a new process invocation on the same Forge session) and is the default path: the
session must have resumable evidence (hook confirmation or transcript-backed state) and must not currently be active.
Reattach mutates confirmed runtime facts (confirmed_at, transcript_path) — the confirmed section reflects "last
seen state." Use --fresh to derive a new child session with context assembly instead.
The session file has three sections:
Schema is intentionally strict: unknown fields and unknown override keys are rejected.
| Section | Definition | Written by | Semantics |
|---|---|---|---|
intent |
Baseline config Forge wants | forge session start |
Session-owned fields only |
overrides |
Live toggles on top of intent | forge session set |
Diff (can be cleared) |
confirmed |
Ground truth of what happened | Hooks | Observed facts, immutable once set |
intent.launch: Forge-owned relaunch preferences for reproducible session launch:
launch:
mode: sidecar
sidecar:
mounts: [/data:/mnt/data:ro]
image: my-dev-image:latestThis keeps forge session resume <name> honest for sidecar sessions without overloading confirmed with user-owned
preferences.
confirmed.started_with_proxy: the proxy this session is running with (set at start, immutable for the run):
started_with_proxy:
proxy_id: my-high-reasoning # optional, same-machine convenience
template: litellm-openai # which template this proxy came from
base_url: http://localhost:8085 # the actual routing identityNormative semantics: proxy_id is optional. The portable fields are template/base_url.
| Term | What it answers | How computed | Stored? |
|---|---|---|---|
effective |
"What should the config be?" | intent with overrides applied |
No (derived on-demand) |
confirmed |
"What actually happened?" | Hooks record facts | Yes (persisted) |
Override rules (for session intent + overrides only):
- Scalars: override replaces
- Lists: override replaces entirely (no concat)
- Dicts: recurse into nested keys (untouched keys preserved)
- Explicit
null: clears the field
Note: There is no "merging"—overrides simply win. The only subtlety is nested dicts: you can override
memory.tagswithout losingmemory.auto_recall. This applies to session-owned fields only (tdd_mode,memory.*, etc.). Proxy-owned fields come directly from the proxy.
- Proxy mode: Claude is configured to send requests to a proxy base URL (
ANTHROPIC_BASE_URL).- The proxy (template ↔ base_url) is the routing identity.
- Status/other tools may query the proxy (
GET /) for tier→model mapping and context windows.
- No-proxy mode: Claude talks to Anthropic directly.
- Sessions, worktrees, hooks, and overrides still work (for session-owned fields).
forge session startandforge session incognitodefault to direct mode. Use--proxyfor proxy routing.forge claude start --no-proxyis a bare launcher (no session state) -- see below.- Tier/model routing doesn't apply—it's proxy-only. Claude Code uses Anthropic models directly.
Normative rule: A session records which proxy it is running with (confirmed.proxy), but cannot override
proxy-owned routing properties. (Proxy requests do not carry a stable session identifier.)
Normative requirement: Launch Claude through Forge. Two launch paths exist:
Session-managed launch (forge session start, forge session resume):
- Requires
.forge/atforge_root(i.e.forge extension enablemust have run -- see project identity model above) - Creates/reuses session state in
<forge_root>/.forge/sessions/ - Sets
FORGE_SESSIONenv var -- hooks and status line can locate the correct session file - Sets
ANTHROPIC_BASE_URLenv var in proxy mode -- routes requests to the correct proxy - Validates preconditions (proxy healthy, session file exists)
- Records
confirmed.proxyat session start when proxy mode is active
Bare launch (forge claude start):
- Convenience proxy launcher -- does NOT create session state
- Does NOT set
FORGE_SESSION-- session-specific hooks, status line session display, and artifacts are all no-ops - Does NOT require
.forge/-- works from any directory - Only sets
ANTHROPIC_BASE_URL(proxy mode) or nothing (direct mode)
Running claude directly bypasses both paths; neither proxy routing nor session integration will work.
See diagrams.md §6: Proxy Routing Flow for a sequence diagram.
To avoid writer conflicts:
- Forge Session (CLI) writes:
~/.forge/sessions/index.json(includesforge_root,checkout_root,project_rootper entry)intent+overridessections in<forge_root>/.forge/sessions/<session_name>/forge.session.jsonintent.launchrecords relaunch mode plus sidecar-specific options (image, extra mounts) when the session is created or derivedconfirmedbootstrap/runtime fields written by the CLI:derivation(resume metadata),is_sandboxed(updated at launch time to reflect whether Claude is running via sidecar)- Sets
FORGE_SESSION=<session_name>when launching Claude - Note:
claude_session_idis not pre-seeded by the CLI; it is set by the SessionStart hook (launch-owned)
- Hooks write:
confirmedsection during the session:claude_session_id, proxy identity, artifacts, policy state, transcript paths. The SessionStart hook is the authoritative source forclaude_session_id.- Locate session via
FORGE_SESSION
- Forge Proxy Orchestrator writes:
~/.forge/proxies/index.json- per-proxy override files (if any)
- Forge Installer writes:
~/.forge/installed.json- installed extension files + merged settings per chosen scope
- Proxy writes:
- proxy-owned snapshot/cache files (if any)
- Status:
- read state; do not invent truth
- Guard:
- reads state; enforces policy decisions at well-defined boundaries (hooks, proxy)
- writes only hook-owned confirmed state (e.g.,
confirmed.policy) when running as a hook adapter
| Concept | Definition |
|---|---|
| Proxy | base_url/port/template + tier→model + default hyperparams. Canonical routing identity for a proxy. |
| Session | Forge-project-scoped intent/overrides/artifacts. May reference a proxy, cannot change proxy-owned fields. |
| Config | In-repo defaults + user credentials/connection values (env vars and/or ~/.forge/credentials.yaml). Connection values (e.g., LITELLM_BASE_URL) bootstrap proxy creation; once proxy.yaml exists, proxy-owned routing is authoritative. |
| Proxy Template | Operational profile defining provider/endpoint/tier-mappings. Internal template for proxy creation. |
| Model Catalog | Authoritative internal data for model capabilities (model_catalog.yaml). Not user-editable. |
| Owner | Fields |
|---|---|
| Proxy-owned | tier→model mappings, provider/base_url, default hyperparams (reasoning_effort, temperature, verbosity, thinking_budget_tokens) |
| Session-owned | policy/TDD mode, memory/artifacts, forge_root, checkout_root, relative_path, session metadata |
| Routing chain | request explicit tier → proxy default tier |
CLI enforcement: Enforced in the CLI: forge proxy edits proxy settings; forge session edits session settings.
Session commands can't set proxy-owned keys.
Implemented:
# List proxies
forge proxy list
# Create a proxy from template with optional per-tier overrides
forge proxy create litellm-openai \
--opus-reasoning high \
--sonnet-temperature 0.7
# Prune stale proxies (dead pids)
forge proxy cleanAlso implemented:
# Start Claude pinned to this proxy
forge claude start --proxy <proxy_id>
# Edit proxy config
forge proxy edit <proxy_id>
# OR: forge proxy set <proxy_id> tier_overrides.opus.reasoning_effort=high
# Delete proxy
forge proxy delete <proxy_id>Key principle: You do NOT edit internal templates/model catalog—only your proxy overlay.
Configuration reference details — proxy overlay schema, template inventory, confusion traps, secrets, runtime config (
~/.forge/config.yaml), model catalog, and status line guidance are in design_appendix.md §A.
When the proxy base URL is reachable, live proxy introspection is authoritative for tier→model mappings and context windows. File caches are allowed but non-authoritative.
The proxy exposes runtime truth via GET /:
{
"is_proxy": true,
"proxy": { "template": "litellm-openai", "base_url": "http://localhost:8085" },
"tiers": {
"haiku": { "model": "gpt-4o-mini", "context_window": 128000 },
"sonnet": { "model": "gpt-4o", "context_window": 128000 },
"opus": { "model": "o3", "context_window": 200000 }
}
}Key points:
- The proxy does not know about sessions (see §3.6.2)
- Session info comes from the session file, not the proxy
- Status line tools read both sources independently
Tier selection precedence:
- Request explicit tier (model name contains
haiku|sonnet|opus) - Proxy default tier (configured for that base URL)
Forge hooks capture session-associated artifacts to make sessions self-contained and inspectable later.
Artifact storage (Forge-project-scoped):
<forge_root>/.forge/artifacts/{session_name}/plans/<forge_root>/.forge/artifacts/{session_name}/transcripts/
Notes:
- Artifacts are scoped to the Forge project root (
forge_root). All sessions in a Forge project share one artifact namespace. - Paths recorded into the session file under
confirmedare forge_root-relative (portable across machines/paths). - Cross-project operations (resume from a different checkout) read parent artifacts by absolute path via
parent_forge_rootin the derivation record (see §3.9).
Plan snapshots:
- We capture approved plan snapshots only (no drafts).
- Approval boundary:
ExitPlanMode. - Snapshot filename includes a timestamp suffix to handle replans (multiple approvals in a session).
Transcript copies:
- We copy the full transcript only at low-frequency boundaries:
Stophook event (session end)/compactor/clearrollover (captured bySessionStartwithsource=compact|clearbefore overwritingconfirmed.transcript_path)
- Destination filename is
{session_id}.jsonl(idempotent per Claude session UUID).
Session file fields (hook-owned, additive):
confirmed.latest_plan_path: pointer to the latest plan file in.claude/plans/…(draft pointer)confirmed.artifacts.plans[]: entries like:{ kind: "approved", captured_at, source_path, snapshot_path }
confirmed.artifacts.transcripts[]: entries like:{ captured_at, reason: "stop"|"compact"|"clear", source_path, session_id, copied_path, copied }
When context nears limits, forge session resume --fresh creates a new session with context assembled from the parent.
It's two-phase: raw artifacts stay immutable; context assembly is flexible.
Phase 1: Handoff (parent session end)
The Stop hook captures everything to artifacts — this is the source of truth:
<forge_root>/.forge/artifacts/<session>/
├── transcript.jsonl # Full conversation (our normalized copy)
├── metadata.json # Confirmed state, lineage pointer
└── plans/ # Approved plans
The hook also updates designated memory docs if work was completed.
Phase 2: Resume (child session start)
The resume command supports two resume modes (--resume-mode):
handoff(default): Assembles parent context into a markdown file passed via--append-system-prompt-file. Lossy but survives/compact(lives in the system prompt). Size controlled by--strategy.native: Uses--resume --fork-sessionto carry full conversation history. Lossless but lost on/compact. No context file generated. Requires the parent to have a confirmedclaude_session_id.
Why not native for worktree forks? Claude Code stores sessions at
~/.claude/projects/<encoded-cwd>/.--resumefrom a different CWD cannot find the session JSONL. Tested with Claude Code 2.1.90 (Apr 2026): all cross-CWD scenarios fail with "No conversation found." Worktree forks use handoff only.
Handoff mode strategies (--resume-mode handoff, default):
forge session resume <parent> --fresh --strategy <strategy> [--depth N]| Strategy | What child session sees |
|---|---|
minimal |
Lineage pointer only — "read parent if needed" |
structured |
Conversation skeleton with truncated tool results |
full |
Complete parent context (fails if exceeds proxy context limit) |
ai-curated |
AI-selected highlights from ancestry chain |
Native mode (--resume-mode native):
forge session resume <parent> --fresh --resume-mode nativeNo context assembly. Full conversation history carried over via Claude's --fork-session.
Context budget enforcement:
Resume knows the target proxy (inherited or via --proxy). For full, it fails fast if the parent transcript
exceeds the proxy context window:
$ forge session resume parent-session --fresh --strategy full
Error: Parent transcript (145K tokens) exceeds proxy context limit (128K).
Use --strategy structured or --strategy ai-curated instead.This prevents launching a session that would immediately hit limits. The check happens before spawning Claude—no wasted tokens or mid-session failure.
For structured and ai-curated, strategies are bounded (truncation/AI selection), so no pre-flight check.
Depth control: Resume can traverse lineage, not just the immediate parent:
depth: 1 # Immediate parent only (default)
depth: 3 # Parent + grandparent + great-grandparent
depth: all # Full ancestry chainThis pulls relevant context from earlier sessions (e.g., a decision from 5 sessions ago).
Processed context location:
<forge_root>/.forge/prev_sessions/<parent-name>.md # Strategy-dependent context view
You can resume the same parent with different strategies. Raw artifacts stay immutable; only the processed view changes.
Session derivation tracking:
Resumes and forks both populate confirmed.derivation; top-level parent_session remains a legacy lookup fallback for
older manifests.
# In confirmed section of forge.session.json
derivation:
parent_session: feature-auth-v1
parent_forge_root: /abs/path/to/parent/forge/root # Where to find parent artifacts
parent_project_root: /abs/path/to/repo # Must match child's project_root
parent_transcript: .forge/artifacts/feature-auth-v1/transcript.jsonl
inherited_proxy: litellm-anthropic # From parent's proxy intent, if inherited
resume_mode: handoff # "native" or "handoff" (authoritative)
strategy: structured # null when resume_mode=native or handoff context not generated yet
depth: 1
resumed_at: 2025-01-02T15:30:00Z
# Lineage chain (computed from parent pointers)
lineage: [feature-auth-v1, feature-auth-v0, initial-planning]Same-directory forks use resume_mode: native, strategy: null, depth: 1, and lineage containing the parent.
Worktree and --into forks start with resume_mode: handoff; the CLI enriches strategy and context_file when it
generates a handoff context file.
Cross-project resume: parent_forge_root tells the child where the parent's artifacts live (may differ from the
child's forge_root when the parent was in a different checkout). parent_project_root must equal the child's
project_root -- cross-repo resume is not supported.
Context assembly (what child loads at start):
- Designated memory docs (always, via CLAUDE.md)
- Processed handoff:
<forge_root>/.forge/prev_sessions/<parent>.md(strategy-dependent) - Lineage reference: pointer to raw artifacts for deep reads
Why two phases?
- Raw artifacts preserve the full history for debugging and audit
- Handoff writes a portable, strategy-dependent view of that history
- Resume assembles context — user controls the fidelity/size trade-off
- Same raw data can produce different views for different needs
Proxy inheritance:
By default, the child inherits the parent's proxy (same routing):
# Parent was on litellm-anthropic proxy (opus)
# Child inherits same proxy by default
forge session resume feature-auth --fresh
# Override if needed
forge session resume feature-auth --fresh --proxy litellm-geminiThis keeps routing stable across resumes.
Hooks write ground truth to the session file. The session manager writes intent (and user overrides); hooks write
confirmed facts (transcript paths, plan paths, proxy identity, etc.).
Session identification: Hooks locate the session via FORGE_SESSION (set at launch), enabling multiple sessions per
Forge project. Hooks use FORGE_SESSION + UUID lookup only. No CWD-based scan or fallback detection.
Implementation: Artifact capture uses first-class hook handlers (testable Python entrypoints), not ad-hoc scripts.
Deployment model: Forge installs hook settings only (no scripts in .claude/). Hooks run via the Forge CLI
(forge hook <name>), so runtime + deps live with the Forge package (single upgrade surface).
Operational requirement: forge must be on PATH for hook execution.
Why forge hook … instead of installed scripts:
- No dependency ambiguity — install Forge once; deps resolved at install.
- No version drift — hooks run the current Forge version.
- Auditable footprint —
.claude/contains config/markdown, not executables. - Testable — regular Python entrypoints (unit-testable, type-checkable).
- Session-aware — reads session file; per-session decisions.
Artifact capture hooks:
forge hook plan-write(PostToolUse:Write): Updatesconfirmed.latest_plan_pathfor plan files.forge hook exit-plan-mode(PreToolUse:ExitPlanMode): Snapshots approved plan to artifacts.forge hook stop(Stop:*): Runs the Stop pipeline (see below).forge hook pre-compact(PreCompact): Captures full transcript before compaction to artifacts. Canonical compaction snapshot; SessionStart rollover is fallback for/clearand defense-in-depth.forge hook post-compact(PostCompact): Records compaction metadata (last_compact_at,last_compact_type).forge hook worktree-create(WorktreeCreate): Replaces Claude Code's defaultgit worktree addto auto-install Forge extensions. Prints worktree path to stdout. Only hook that exits non-zero on failure.forge hook subagent-stop(SubagentStop): Tracks subagent activity (total_count,by_type, transcript path, message preview). Observe-only (phase 1).
Stop hook pipeline:
The Stop hook does multiple things. To avoid blocking exit and ensure idempotency across repeated invocations, it's split into sync and async phases:
Stop Pipeline:
[Sync - blocks exit decision, must be <100ms]
1. capture_artifacts() Copy transcript to .forge/artifacts/ (idempotent via UUID)
2. run_verification() Check completion promise → returns allow|block
[Async - enqueued, fire and forget]
3. enqueue_handoff_work() Mark session for handoff agent + indexing
return verification_decision
The handoff agent runs async to avoid blocking exit. Memory doc updates are eventually consistent—fine since they benefit future sessions.
Idempotency rules (verification can trigger Stop multiple times per session):
| Step | Multiple invocations safe? | How |
|---|---|---|
| Artifact copy | ✔ Yes | Writes to UUID-named path, overwrites are identical |
| Verification | ✔ Yes | Stateless check of last message |
| Async enqueue | ✔ Yes | Marker file is idempotent (same content = no-op) |
Async enqueue: The Stop hook enqueues a marker via enqueue_stop_marker() for deferred processing. See §3.13 (Async
Work Queue) for the queue contract, schema, and processing model.
This keeps the Stop hook fast (<100ms) while ensuring handoff + indexing happen soon.
Design rule: hooks emit machine-readable JSON; no systemMessage required (handoff agent replaces manual reminders).
Forge supports a direct command channel to invoke Forge actions inline from the Claude prompt without adding slash commands or changing hook wiring.
Design goal: install one UserPromptSubmit hook, then add new %<cmd> handlers over time without
reinstalling hooks.
⚠︎ Limitation:
UserPromptSubmithooks only fire in interactive Claude sessions. They do NOT fire inclaude --printmode (non-interactive/piped).%commands working in
Mechanism:
- Claude Code
UserPromptSubmithook runs:forge hook user-prompt-submit - The handler parses prompts that begin with
%and dispatches to the appropriate command implementation. - Unknown
%<cmd>strings are ignored (normal Claude flow continues).
Response contract:
- When a direct command is handled, the hook returns a Claude Code decision payload:
{ "decision": "block", "reason": "..." }
- When not handled, it emits no output and exits successfully.
Scope policy: % commands are primarily session-scoped. Proxy commands are restricted to read-only operations
because proxies are global (modifying a proxy mid-session could affect other sessions using the same proxy). Proxy
management should be done deliberately from terminal.
Full command list and scope policy table in design_appendix.md §B.
Forge implements "Shared" operations once in a UI-agnostic command-core layer and exposes them via both:
- terminal CLI (
forge ...), and - direct commands (
%...viaforge hook user-prompt-submit).
Location: src/forge/core/ops/
Contract: ops contain pure logic (no Click, no printing, no hook JSON). They return structured data and raise typed exceptions on failure.
This avoids duplicating business logic between terminal and in-session entry points.
A general-purpose, file-based queue for deferred work. Producers enqueue markers; CLI startup processes them opportunistically. This is a core primitive used by the Stop pipeline, search indexing, and handoff agent.
Module: forge.core.workqueue
Queue location: ~/.forge/pending-work/ (respects FORGE_HOME)
- Best-effort enqueue: failures are non-fatal (never block hooks or CLI)
- Fast path: no-op when queue is empty (cheap directory scan)
- Concurrent-safe: per-marker advisory locks (
<marker_id>.json.lock) - Exactly-once-ish: markers deleted on successful handler completion
- Eventually consistent: deferred work benefits future sessions, not the current one
Each marker is a JSON file with kind (routing key), marker_id (idempotency key), payload (kind-specific data), and
retry tracking (attempt_count/last_error). Handlers are passed as an explicit dict (no global registry). Successful
handling deletes the marker; poison markers (5+ attempts) move to pending-work/failed/.
Marker schema, processing contract, and known kinds in design_appendix.md §C.
The primary entry point for all Forge operations.
Command aliases: authentication (canonical) has alias auth; extension (canonical) has alias ext and
extensions; session (canonical) has alias sess. Full names always work; aliases are convenience shortcuts.
Command-shape policy: Forge uses explicit verbs for all commands. config and search support
invoke_without_command as convenience defaults (config shows effective config, search -q runs a query). All other
groups require an explicit subcommand. List/show commands support --json for scripting.
| Command | Purpose |
|---|---|
forge extension enable |
Install Forge extensions (commands, agents, hooks, settings) |
forge extension sync |
Update existing installation to current version |
forge extension disable |
Remove Forge installation cleanly |
forge extension status |
Show installation status (--json) |
| Command | Purpose |
|---|---|
forge session start [name] |
Create and start a new session (auto-named if omitted) |
forge session resume [name] |
Reattach to an existing session (default), or derive a fresh child with --fresh |
forge session fork <parent> [--name] |
Fork a session (same dir by default; --worktree for isolation) |
forge session show [session] |
Show session details (--json, --field); accepts name or UUID |
forge session list |
List sessions (--scope repo|project|all; default repo; --json) |
forge session set <key> <value> |
Set a mid-session override |
forge session reset [key] |
Reset overrides to intent |
forge session delete <name>... |
Delete one or more sessions (--all for bulk deletion) |
forge session clean --older-than N |
Bulk-delete sessions older than N days |
forge session incognito [name] |
Start an ephemeral session (auto-delete on exit) |
forge session shell [name] |
Open shell in sidecar container |
Note: session context is a deprecated alias for session show.
| Command | Purpose |
|---|---|
forge proxy create <template> |
Create a proxy from template and start it |
forge proxy list |
List all proxies (--json) |
forge proxy show <id> |
Show proxy configuration (--json, --raw) |
forge proxy edit <id> |
Edit proxy overlay in $EDITOR |
forge proxy set <id> <key>=<value> |
Set a proxy configuration value |
forge proxy start <id> |
Start server for existing proxy |
forge proxy stop <id> |
Stop server (keeps config) |
forge proxy delete <id>... |
Delete one or more proxies (--all for bulk deletion) |
forge proxy clean |
Remove stale proxies (dead pids) |
forge proxy validate <id> |
Validate proxy configuration |
forge proxy metrics [id] |
Show runtime metrics (--json, --all) |
forge proxy template list |
List available templates |
forge proxy template show <name> |
Show template configuration (--raw) |
forge proxy template edit <name> |
Customize a template (copy-on-first-edit) |
forge proxy template reset <name> |
Reset template to built-in defaults |
| Command | Purpose |
|---|---|
forge claude start --proxy <id> |
Launch Claude configured for a proxy |
forge claude start --no-proxy |
Launch Claude without proxy (Anthropic API) |
forge claude preset show |
Show current settings preset (--raw) |
forge claude preset edit |
Edit settings preset in $EDITOR |
forge claude preset reset |
Reset preset to built-in defaults |
| Command | Purpose |
|---|---|
forge backend list |
List backends (--json) |
forge backend show <adapter> |
Show backend details (--raw) |
forge backend create <adapter> |
Create backend config |
forge backend start <adapter> |
Start backend instance |
forge backend stop <adapter> |
Stop backend instance |
forge backend delete <adapter> |
Delete backend instance or config |
| Command | Purpose |
|---|---|
forge guard enable --bundle <name> |
Enable policy enforcement for current session |
forge guard disable |
Disable policy enforcement |
forge guard status |
Show current policy state (--json) |
forge guard list |
List available bundles and rules (--json) |
forge guard check --bundle <name> -f <path> |
Evaluate policies on demand |
forge guard supervisor -f <path> -r <id> |
Evaluate file against approved plan |
forge guard supervise <target> |
Set persistent supervisor for session |
forge guard supervise --off / --on |
Suspend/resume supervisor (preserves config) |
forge guard supervise --remove |
Remove supervisor entirely |
forge guard supervise --reload |
Reload latest relevant approved plan |
forge guard supervise --reload-from <path> |
Reload plan from explicit file |
| Command | Purpose |
|---|---|
forge workflow panel [targets] |
Fan out review to multiple models |
forge workflow analyze [topic] |
Deep single-model analysis |
forge workflow debate [subject] |
Adversarial evaluation with stance workers |
forge workflow consensus [subject] |
Two-round multi-model convergence |
forge workflow list-models |
Show available model backends |
| Command | Purpose |
|---|---|
forge search -q <query> |
Search transcripts |
forge search rebuild-index |
Full index rebuild from artifacts |
forge search status |
Show index statistics |
forge search clean |
Remove orphaned documents from index |
| Command | Purpose |
|---|---|
forge info |
Show global system information (--json) |
forge clean |
Remove orphaned state (--scope, --yes) |
forge config |
Manage global runtime preferences |
forge authentication login |
Store credentials for LLM providers |
forge authentication status |
Show credential status per provider |
forge logs |
Show log file locations and status |
Internal (hidden from forge --help)
| Command | Purpose |
|---|---|
forge hook <name> |
Hook dispatcher (SessionStart, Stop, etc.) |
forge status-line |
Generate status line output |
forge handoff run |
Run handoff agent for completed session |
Design principles:
- Narrow global config --
forge configowns runtime preferences only; routing stays per-proxy and workflow state stays per-session - Explicit verbs -- all groups require a subcommand (
configandsearchare the two exceptions withinvoke_without_commanddefaults) - Launch through Forge --
forge session start,forge session resume, orforge claude start --proxysets up env vars correctly
Forge Policy is an enforcement system with three types:
- Deterministic Policy (Guard): Static checks, file mapping, dependency rules (Fast/Free).
- Semantic Policy (Supervisor): LLM-based alignment checks against plans (Smart/Context-aware).
- Verification Policy: Outcome-based checks at session boundaries (Feedback loop).
| Policy Type | Boundary | Question |
|---|---|---|
| Deterministic | PreToolUse | "Is this action allowed?" |
| Semantic | PreToolUse (throttled) | "Is this aligned with the plan?" |
| Verification | Stop | "Did it achieve the goal?" |
Definition: a policy is an enforcement function that runs at a well-defined boundary (hook, proxy, commit hook) and returns an action decision with an explanation.
At minimum:
- Input: an action context (what is about to happen)
- Output:
allow | warn | deny | needs_reviewplus human-readable reasons.needs_reviewis an intermediate decision: the semantic supervisor must resolve it toallow,warn, ordeny; if no configured supervisor resolves it, the hook blocks as unresolved. - Intent: every policy declares why it exists — shown to models on deny so they can distinguish good workarounds (satisfy the goal) from bad ones (defeat it)
Forge Policy is designed to support deterministic policies first.
- Engine: policy interfaces, composition, decisions
- Adapters: hook boundary (no-proxy) and proxy boundary (proxy-mode)
- Policy bundles: TDD is expressed as a set of deterministic policies (e.g., "tests must exist before implementation").
Base class contract (DeterministicPolicy):
| Abstract property | Type | Purpose |
|---|---|---|
policy_id |
str |
Unique identifier (e.g., tdd.tests-before-impl) |
description |
str |
Human-readable description |
intent |
str |
Why this policy exists — shown on deny so models understand the goal |
All three are required (abstract). The intent field was added after observing that models (e.g., GPT-5.5) would
find creative workarounds that pass the check but defeat the goal (Unicode escapes to bypass byte-level emoji
detection). Showing the intent alongside the violation steers models toward compliant approaches or surfacing conflicts
to the user.
Why enforce coding standards via policy? AI assistants tend to favor gradual migration and backward compatibility
over clean breaks—even when explicitly instructed otherwise. Patterns like warn+ignore, fallback logic, and
compatibility shims sneak into codebases despite best efforts. Deterministic policies can catch these at commit/hook
boundaries:
- Reject code containing
# backward compat,# legacy,# deprecatedcomments - Flag new
if TYPE_CHECKING:blocks (circular import workaround) - Detect
warn+strip/ignorepatterns in validation code
This doesn't require a stateful system—detecting backward compat patterns in a diff is a pattern-matching task that Haiku handles fine.
Policy bundles group related rules:
| Bundle | Rules | Purpose |
|---|---|---|
tdd |
tests-before-impl, no-skip-tests | Test-driven development |
coding_standards |
no-bsd-sed, no-type-checking, no-backward-compat | Platform/style rules |
Bundles are enabled per-session:
# In session intent
policy:
bundles: [tdd, coding_standards]
tdd_mode: strict # off | permissive | strictThis enables "Active Alignment" checking using the Side-Channel Architecture.
Promotion flow (--fork-current) is deferred so the building blocks (supervisor, panel, session forking) can compose
before we hardcode a default. For now: forge session set policy.supervisor.resume_id <uuid>.
Mechanism: "CLI-Fork Supervision"
The policy-check hook runs the supervisor via claude -p --resume <supervisor_id>:
- Configure:
forge session set policy.supervisor.resume_id <uuid>(from planning session). - Check: Runs at PreToolUse for Write/Edit, throttled via cache (default 30s).
- Enforce:
- Aligned: Silent success (cached for throttle window).
- Divergent + high confidence + citations: Block the tool (exit 2).
- Divergent + low confidence or no citations: Warn via stderr, allow the tool.
- Unresolved review request: Block the tool (exit 2) until a supervisor is configured or the user gives a new direction.
Why this works: Forking the planning session makes the plan's original context the enforcement authority (no RAG). Side-channel architecture: Executor uses a high-IQ coder (Opus); Supervisor uses a high-context checker (Gemini).
Promotion readiness: Depends on ground truth quality: explicit acceptance criteria, invariant constraints, resolved ambiguities.
Supervisor lifecycle controls:
--off/--on: Toggle without config loss.--offsetssuspended=True(config preserved, hook skips evaluation entirely — not registered in the policy engine);--onresumes.--removeis the destructive path. Both CLI and direct command surfaces. All three pre-check that a supervisor is configured before acting.--reload/--reload-from <path>: Inject an updated plan into supervisor evaluation context.--reloadsearches the supervision graph in order: current supervised session, related forks (sessions in the sameforge_rootwhose parent is the supervisor target), supervisor target session. Only approved snapshots are considered (no drafts). The plan content is prepended to each evaluation prompt with explicit supersession framing.--reload-fromtakes an explicit file path (resolved relative to CWD, stored absolute). Cache key includes apath:mtime_ns:sizefingerprint so in-place edits invalidate cached verdicts.plan_override_pathonSupervisorConfigstores the override. It can be set while the supervisor is suspended (configure plan, then--on). Proxy routing is not re-seeded on--on— the preserved config is used as-is.- Auto-reload may succeed even if the supervisor target session has been deleted (the current session or a related fork
may still hold the plan). Status surfaces show
Target: <name>when resolvable and omit it otherwise.
Supervisor stuck playbook: When the supervisor blocks because the plan evolved:
%guard supervise off(suspend, config preserved)- Make the approved changes
%guard supervise reload(searches current session, forks, then target) or%guard supervise reload <path>%guard supervise on(resume with updated plan context)
The underspecification problem (biggest failure mode): Supervision catches explicit divergence (plan says X, agent did Y, citations are clear). Underspecification is harder: the plan is silent, the model picks a plausible default, and neither agent nor supervisor can cite against it — so the verdict may be "aligned." The real divergence is between unwritten human expectations and model assumptions. Mitigation: (a) more explicit plans (write down the implicit), (b) multi-model review (different defaults expose gaps), (c) the human reformulation loop (make assumptions explicit, rerun).
Operational reliability constraints (normative):
- Citations required: Every Divergent finding MUST cite (quote) the specific plan/design section it violates.
- Structured verdict: The Supervisor response SHOULD be parseable (even if implemented as plain text initially):
verdict:aligned | divergentviolations[]:{ severity, evidence, suggested_fix, citations[] }
- Block only on high-confidence + cited rule: Default behavior is warn-only unless the Supervisor provides a clear cited rule and a high-confidence violation.
- Fail open vs fail closed: Policies MUST define failure behavior per severity (e.g., CLI failure, proxy down, timeout). Default to fail-open (warn-only) for most checks. Fail-open for policy evaluations is a system-boundary rule (LLM output is external data), not an exception to coding-standards §5. See coding-standards.md §5 (boundary framework) for the general framework.
- Throttling + caching: Supervisor checks SHOULD be throttled (e.g., every N turns, only on Write/Edit, only for configured path prefixes) and MAY cache the last verdict for identical diffs.
On-demand invocation (planned): Every policy — including the supervisor — should be callable manually without installing hooks:
forge guard check supervisor --file src/forge/session/store.py
forge guard check tdd --diff HEAD~1The same evaluation function runs in both modes (hook-triggered and CLI-triggered).
Primary use case: problem reformulation. When a policy stops the agent, the cause is flawed problem representation
(too broad/contradictory/ambiguous), genuine agent failure, or an overzealous policy. On-demand checks are diagnostics:
citations and evidence show what failed. Reformulate, then re-check before resuming. The supervisor's --resume
context keeps the original plan in view.
Reactive Patterns (Shared Library)
Several components react to hook events via external processing: semantic supervisor (guard/semantic/supervisor.py),
handoff agent (session/handoff_agent.py), deterministic policies (guard/deterministic/), and the planned workflow
policy. The shared pattern: take hook context, classify/evaluate, return a decision or side-effect. Three node types
cover current and planned use cases:
| Node type | Execution | Examples | Cost |
|---|---|---|---|
| Code | Deterministic Python function | TDD enforcement, path gating, file checks | Free |
| LLM call | Stateless API call via core.llm |
Tagger (classification), checker | ~$0.001 |
| Claude session | claude -p [--resume] subprocess |
Supervisor, handoff agent | ~$0.01-0.05 |
Library, not framework: Utilities live in a shared Python library (core/reactive/). Hook handlers are plain Python
functions that import what they need. No YAML workflow engine, no declarative config layer — the same developers who
would write YAML can write Python with less indirection and better debuggability.
The shared library provides utilities extracted from existing implementations: session runner, proxy resolution, throttle cache, structured output parsing, tagger, env builder, fan-out runner, and adversarial runner. A developer adding a new policy imports these utilities and writes a class.
Shared library API table and example policy code in design_appendix.md §D.
WorkflowPolicy (tagger → branch → checker → reviewer): Plugs into PolicyEngine via existing
Policy + StatefulPolicy protocols (zero changes to the engine). Composes library utilities into a branching pipeline:
a shared tagger classifies the action, branches match by tags (first match wins), each branch has optional filter →
checker → reviewer stages. The tagger is called once per event and its tags route to all matching downstream checks —
avoiding redundant classification.
Team extension: The same library works for team hooks (TeammateIdle, TaskCompleted) by subscribing to different
events. See team_design.md §3.
Verification policies check outcomes rather than actions. They run at the Stop boundary and can block session exit until goals are achieved.
The Ralph-Wiggum Pattern:
Instead of external bash loops, verification uses the Stop hook to create a self-referential feedback loop:
- User starts session with a completion promise
- Claude works toward the goal
- Stop hook checks: "Did Claude output the completion signal?"
- If no → block exit, re-inject prompt, continue
- If yes → allow exit
The prompt never changes between iterations, but Claude's previous work persists in files. Each iteration sees modified files and git history, enabling autonomous improvement.
Configuration:
# In session intent
verification:
type: completion_promise # or: test_suite, custom_command
promise: "<done>COMPLETE</done>"
max_iterations: 50 # safety limit
on_incomplete: re_inject # or: warn, allow
re_inject_prompt: |
Continue working. Output <done>COMPLETE</done> when all requirements met.Verification types:
| Type | Verification method | Use case |
|---|---|---|
completion_promise |
Look for text in output | Goal-driven tasks |
test_suite |
Run tests, check exit code | Code changes |
custom_command |
Run any command | Domain-specific |
Completion promise correctness:
To avoid false positives (promise appearing in quoted files, code examples, or earlier failed iterations):
- Check only the last assistant message — ignore tool results and conversation history
- Require standalone line — promise must appear on its own line, not embedded in prose
The re-inject prompt should instruct Claude accordingly:
re_inject_prompt: |
Continue working. When ALL requirements are met, output this on a standalone line:
<done>COMPLETE</done>This prevents false matches from print("<done>COMPLETE</done>") in code or discussion like "I'll output
<done>COMPLETE</done> when done."
Escape hatches: %cancel-verification direct command, max_iterations auto-bypass, max_minutes wall-clock limit,
or forge session set verification.bypass true from another terminal. The bypass is a session parameter (discoverable,
auditable). Both time limits matter: max_iterations catches fast-failing loops; max_minutes catches slow token burn.
Why verification is policy, not a separate concept:
- All three policy types share the same structure: boundary + check + action
- Verification just fires at a different boundary (Stop vs PreToolUse)
- Keeps the design unified under "Policy"
Policies operate on a normalized view of what Claude Code is doing, for example:
- hook event (
PreToolUse.Write,PreToolUse.Edit, …) - tool arguments (target path, content/diff metadata)
- repository/worktree path
- effective session config (intent + overrides)
Multiple policies may run for a single action:
- Any deny in enforce mode blocks the action
- warnings accumulate
- results can be logged for audit/debug
Deny message format (three-tier, shown to the model):
Policy violation(s):
[rule_id] violation message
Intent: why the policy exists
Fix: suggested fix (if available)
Note: This policy was configured by the project owner. First try a
compliant approach that satisfies the intent above. If the user's
request cannot be fulfilled without violating the intent, explain
the conflict and ask how to proceed. Do not attempt bypasses that
pass the check but defeat the goal.
The Intent: line appears once per denying policy (not per violation). The Note: uses project-owner framing so models
treat it as a constraint to respect, not an obstacle to circumvent.
Policy has two aspects with different ownership: definition (configuration — who sets the rules) and state
(runtime — what happened). Supervisor model and throttling are proxy-owned (routing decisions). TDD mode, policy
enabled/disabled, and verification config are session-owned (workflow decisions). All enforcement results are
hook-written to confirmed.policy.
Policy provenance: confirmed.policy records forge_version, bundles, rules_active, and decisions for
audit/debugging ("why did this block?").
Ownership rationale: Supervisor model = routing decision → proxy. TDD mode = workflow decision → session.
Enforcement results = observed facts → hook-written confirmed. Stateful policies (e.g., "tests touched") write only to
confirmed.policy.
Full policy definition and state ownership tables in design_appendix.md §D.
Claude Code extensions live in this repo and are installed via forge extension enable. Forge follows Claude Code's
scope model (--user / --project / --local) and provides modular installation via profiles (minimal / standard
/ full). Six installable modules (commands, agents, skills, hooks, status-line, permissions) are combined into
profiles. Settings merge is additive (hooks append + dedupe, permissions union). ~/.forge/installed.json tracks what
was installed for clean update/uninstall. Project/local enablement requires a .claude/ anchor at the target directory
(created if missing); user-level install (--user) goes to ~/.claude/ and does not require a project anchor. This
establishes the Forge project per the identity model (§3).
Scope model, module inventory, merge rules, and tracking file details in design_appendix.md §E. Multi-scope installation behavior (dual user + project) is documented in §E.5.
Skills are Forge's scripting layer: they teach Claude to compose Forge capabilities into workflows. Game engines
have Lua; editors have VimScript; Forge has skills. The forge CLI is the engine (proxy routing, session management,
core.llm). Skills are the instructions; the agent orchestrates.
Skills don't add tools—Claude already has Read/Write/Bash. Skills add the playbook for composing them with Forge (multi-proxy routing, session forking, policy checks).
Forge installs skills about itself: forge extension enable deploys CLI commands (capabilities) and skills (how to use
them). The system teaches the agent about itself.
- Coherent upgrades:
forge extension syncupdates CLI + skills atomically - No version drift between "what the tool can do" and "what the agent thinks the tool can do"
- Agents can modify skills (markdown files on disk)
- Matches hooks/status-line pattern:
forgeis the engine; extensions are instructions
Forge uses skills (not MCP tools) for agent workflows. MCP servers remain useful for external data access (APIs, databases, OAuth), but aren't the right abstraction for workflow orchestration.
| Aspect | Skills | MCP Tools |
|---|---|---|
| Token cost | Typically ~100 tokens metadata at startup | Often 3K-10K+ for tool definitions |
| Context pollution | Full instructions load only when invoked | Tool schemas persist in context |
| Architecture | Reflective — skills reference own install | External — separate server process |
| Context passing | Fork session (full context preserved) | Summarize and send (information loss) |
| Determinism | Agent interprets instructions each time | Structured JSON-RPC interface |
Fork advantage: Skills can fork the current session (claude -p --resume <uuid> on another proxy), giving reviewers
the full conversation context (files, decisions, rationale). MCP tools only see what the agent summarizes into tool
parameters.
Skills that invoke multi-model workflows support two context modes; the caller chooses based on the situation — skills present options, not prescriptions.
Resume context mode: claude -p --resume <session-uuid> on another proxy; inherits full session context. Best when
conversation history matters. Requires Claude Code >= 2.1.80 for reliable parallel tool result handling (--resume
dropped parallel tool results in earlier versions).
Blind context mode: Fresh claude -p (no --resume); rely on the prompt + filesystem reads. Cheaper and
independent. Best for isolated reviews and quick checks.
CLI contract: Workflow CLIs expose this axis explicitly:
--context resume:<session-uuid>→ pass--resume <id>to each worker--context blind→ do not pass--resume(workers are independent)
CLI surface: forge workflow <workflow> (e.g. panel/analyze/debate). Default is human-readable; --check produces
a policy-grade verdict (JSON + exit code).
Skills vary in what they execute. Four types cover all current and planned cases:
| Type | Execution | Examples |
|---|---|---|
| Pure Python | Deterministic function | TDD guard, pattern matching, run_tests() |
| Single LLM | core.llm API call |
Tagger, checker |
| Claude session | claude -p [--bare] subprocess (has tools) |
Supervisor, reviewer, handoff agent |
| Pure text (.md) | Markdown instructions sent to claude -p |
Review resources, analyze, debate prompts |
Claude session subprocesses use --bare when ANTHROPIC_API_KEY is in the environment (skips hooks, LSP, plugin sync,
skill walks for faster startup). --bare disables OAuth/keychain auth, so it is only safe when an explicit API key is
available. They are full Claude Code agents (Read/Grep/Bash/Write) but cannot invoke skills that spawn more subprocesses
(FORGE_DEPTH prevents recursion at depth >= 2 as defense-in-depth). Pure text (.md) is a markdown prompt run in that
environment.
Skills can declare effort: high|medium|low in their SKILL.md frontmatter (Claude Code 2.1.80+). This overrides the
model effort level when the skill is invoked -- useful for deep-analysis skills (analyze, debate) that benefit from
maximum reasoning. This is orthogonal to proxy-level reasoning_effort hyperparameters, which control the routed
model's behavior.
This maps to the three node types in §4.1.2 (Code, LLM call, Claude session). "Pure text" is a specialization: no Python runtime deps, so the prompt is portable across models/runners (the execution environment still has tools).
Multiple skills compose smaller skills into orchestrated loops. Forge recognizes a small set of fundamental workflow
runners: reusable Python functions in core/reactive/ that each implement one loop pattern.
Three-layer architecture:
| Layer | What | Lives in | Examples |
|---|---|---|---|
| 1 | Abstract runners | core/reactive/ |
Fan-out, adversarial, linear, actor/critic |
| 2 | Skill resources | src/skills/*/resources/ |
Review resource .md, analyze prompt, tagger |
| 3 | Concrete invocations | src/skills/*/SKILL.md |
/forge:panel, /forge:debate, /forge:review |
Layer 3 entry points wire a runner (Layer 1) to specific resources (Layer 2). The same runner/resource can be combined differently by different entry points.
Four fundamental runners (conservative set):
| Runner | Loop pattern | Status | Current implementation |
|---|---|---|---|
| Linear | A → B → C (sequential) | Exists | WorkflowPolicy pipeline, Stop pipeline |
| Fan-out/Fan-in | N workers parallel → collect → synthesize | Exists, enhancing | run_multi_review() |
| Adversarial | N workers with stances, blinded → synthesize | Exists | run_adversarial() |
| Actor/Critic | Generate → critique → iterate | Pattern exists | Ralph-wiggum verification loop |
Design principles:
- Python, not YAML — continues the "library, not framework" approach
- Each runner takes skills, returns structured output
- Callable from skills (on-demand) and policies (automatic)
- Conservative set: fundamental patterns only
The fan-out runner (run_multi_review()) spawns N workers in parallel via ThreadPoolExecutor, each with its own
model/proxy and optional per-worker prompt. The adversarial runner constrains workers to review/eval skills with
stance injection ({stance_prompt}), mandatory blinding (no peer outputs), and evidence-weighted synthesis.
Skills and policies are the same building blocks with different triggers:
| Policies | Skills | |
|---|---|---|
| Trigger | Hook event (automatic, on Write/Edit) | Agent/user invocation (on demand) |
| Output | Allow/deny decision | Information for agent to synthesize |
| Latency | Adds overhead to every action | Zero overhead until invoked |
| Use case | Continuous enforcement | Deliberate checks |
Both compose from the same primitives: core/reactive/, core.llm, run_claude_session(). Shared code is imported by
both CLI commands and policy classes—no workflow registry, no declarative config layer. Library, not framework.
CLI surfaces (normative): Forge uses two related command surfaces:
-
Guard — deterministic/semantic policies evaluated against an action context.
- Hook surface:
forge hook …invokes Guard policies automatically. - Manual surface:
forge guard check …runs Guard policies on demand (after a hook blocks you, or in CI).
Stuck playbook (target UX): When a PreToolUse policy blocks repeatedly, give the human an escape hatch without uninstalling hooks.
- Disable enforcement in-session:
%guard disable(hook becomes a no-op for this session) - Fix the issue: work with the agent or edit manually while enforcement is disabled
- Confirm you're unblocked (optional):
%guard check(planned): defaults togit diff(unstaged). Supports--staged.- Terminal fallback:
git diff | forge guard check --bundle tdd --bundle coding_standards --diff
- Re-enable enforcement:
%guard enable(planned): with no bundles, restores the session's configured bundles from intent.%guard enable tdd coding_standards: explicitly sets bundles for the session.
forge guard check(and%guard check) are diagnostics; you're unstuck once enforcement is re-enabled and the next Write/Edit passes the hook. - Hook surface:
-
Run -- multi-step workflow runners (fan-out, debate, etc.).
- Default:
forge workflow <workflow>returns a human-readable result. - Gate mode:
forge workflow <workflow> --checkforces a policy-grade verdict contract (structured JSON + exit code).
- Default:
No auto-promotion: A workflow does not automatically appear in Guard. If a Guard policy wants to use a workflow, it
invokes the workflow's --check surface explicitly.
Workflow runners unify skills and policies. The same runner is usable from:
- Skills (agent/user invoked)
- Hooks/policies (automatic gate via
--check) - CLI manual runs (human debugging)
Panel is the reference invocation of the fan-out runner. It fans out a review task to N models via different proxies and collects independent findings for synthesis. Each reviewer is a full Claude Code agent (can read files, investigate, find issues with real file:line evidence). The main agent synthesizes all N reviews -- identifying consensus findings, unique insights, and conflicts -- with full project context to investigate disputes.
Dual use: The panel serves as both a skill (/forge:panel src/session/ --code) and a policy (automatic multi-model
gate before committing). Same run_multi_review() function, two callers -- the programmer wires both. /forge:analyze
is a degenerate fan-out (N=1) with an analyze-specific resource.
Debate is the reference invocation of the adversarial runner. It assigns stances (for/against/neutral) to workers,
blinds them from each other (separate claude -p processes, no --resume), and synthesizes by weighing agreement
against disagreement. Stances influence the evaluative lens, not honesty -- all stances include ethical guardrails. Only
review/evaluation skills are adversarial-compatible (runner checks for {stance_prompt} marker).
Detailed runner configs, debate protocol, and operational constraints (recursion guard, JSON output contract, child process lifecycle, script dependency tiers) in design_appendix.md SF.
Cross-session continuity via designated markdown files that sessions keep updated—no knowledge graphs or async synthesis.
The simplest memory system is:
- Designated markdown files with templates
- Sessions read them at start (via CLAUDE.md references)
- Sessions update them before ending
- Next session gets current state
A headless agent runs at session end to fill gaps automatically:
Stop hook → spawn headless agent → agent reads transcript + current docs → updates
The agent runs claude -p (headless prompt mode) on the full session transcript. It operates retrospectively,
selecting what mattered with full-session hindsight (higher signal than incremental capture).
# In session intent or project config
memory:
auto_update:
enabled: true
mode: augment # augment (add missing) | review-only (dry run)
proxy: litellm-haiku # cheap model for summarization
min_turns: 5 # skip for very short sessionsMulti-agent workflow: In parallel runs, each agent spawns its own handoff agent. augment mode stays additive (no
overwrites).
The handoff agent has two distinct modes for designated docs:
Mode 1: Direct Update — agent updates the doc in place per strategy. Used for project docs the agent is allowed to maintain.
Mode 2: Shadow/Propose — the agent is the proposer, the human is the author. It reads transcript + official doc, proposes additions to a shadow doc that aren't already in the official doc; the human reviews and merges at their own pace.
The mode is determined by the shadows field on DesignatedDoc:
@dataclass
class DesignatedDoc:
path: str # Target file to write
strategy: str = "generic" # How to update it
shadows: str | None = None # If set: official doc this proposes changes forWhen shadows is set, the prompt reads the official doc first and proposes only what's missing (avoids redundancy).
Per-doc strategies control how each file is updated. Strategies are a str → str dict (extend without code changes).
No file creation. Designated docs must already exist; missing files are skipped. Humans choose which docs to maintain; the agent maintains them. This avoids the agent making structural choices (new files/templates) implicitly. Seed files before configuring them.
Direct update strategies (Mode 1) include: project-state, checklist, changelog, debugging, patterns,
generic. Shadow strategy (Mode 2): suggested (propose additions as checkboxes with rationale).
The handoff agent resolves designated doc paths relative to forge_root (managed sessions always launch from
forge_root), so git-tracked docs target the correct branch in worktrees. Trackedness is controlled by path choice --
the agent doesn't distinguish.
Relationship to Claude Code auto-memory: Complementary, not competitive. Auto-memory captures during sessions (incremental, free-form); the handoff agent synthesizes after sessions (retrospective, per-doc strategies). The handoff agent deliberately does not read auto-memory — different targets, different information, occasional duplication is cheaper than cross-format deduplication.
Strategy tables, example config, worktree resolution details, and full auto-memory comparison in design_appendix.md §G.
Runtime architecture (host-based): Proxy runs on host (subprocess.Popen), Claude Code runs on host. End users do
NOT need Docker.
Test infrastructure (Docker-based): Integration tests run inside Docker containers (developers/CI only) to ensure:
- No Dockerfile/fixture drift (single source of truth)
- Tests catch real bugs (e.g., proxy startup failures)
- Deterministic test environment across machines
Test workflow:
# Unit tests (no Docker needed)
uv run pytest tests/src -m "not integration"
# Integration tests (Docker required for developers/CI only)
make test-integration # Runs: docker build + docker run pytestAutomated tests catch logic bugs but miss UX/latency/real-system failures. Previous manual testing found 5 real bugs (including a macOS crash) that ~2,400 automated tests missed.
Why checklist-driven. Early versions let the agent improvise commands — producing invented CLI commands, interactive prompts that hang the Bash tool, and leaked API keys. The fix: pre-written checklists where commands and assertions are deterministic and the agent only interprets results. Checklist edits change tests without modifying skill instructions.
Three skills with escalating isolation, tied to install profiles:
| Skill | Profile | Isolation | Audience |
|---|---|---|---|
/forge:smoke-test |
standard |
Host, read-only probes | End users |
/forge:walkthrough |
standard |
Host, hermetic test repo (--sidecar adds Docker) |
End users / demos |
/forge:qa |
full |
Docker container | Maintainers |
Shared pattern — checklist + wrapper + annotations. Each skill reads a checklist, runs commands through a mode-specific wrapper, and routes items by annotation. A three-window model (Session A runs the skill, Session B is the subject under test, Terminal for raw CLI) enables interactive verification of things the agent can't see. Session A prompts the user to open Terminal early. Session B is launched only when the checklist first needs interactive verification.
Key design decisions:
- Share the pattern/convention, not the prompt — each skill is self-contained (no cross-mode confusion)
- Checklist is single source of truth — editing it changes tests without SKILL.md modifications
walkthrough-state.pyis the deterministic bookkeeper — agent classifies (pass/fail/skip), script counts- No per-checklist-item scripts — wrapper + lifecycle scripts are enough
/forge:qatied tofullinstall profile (Docker dependency)
Annotation types, wrapper details, and per-skill specifications in design_appendix.md §I. See also testing-guidelines.md for the full testing reference.
claude-forge/
├── src/
│ ├── forge/ # Python package
│ │ ├── core/ # Shared libraries
│ │ │ ├── llm/ # LLM client abstraction (see design_appendix.md §J)
│ │ │ ├── auth/ # Auth flows (LiteLLM, credential store)
│ │ │ ├── models/ # Model catalog (forge.models.yaml)
│ │ │ └── state/ # File-based state helpers
│ │ ├── session/ # Session manager
│ │ ├── install/ # Installer system
│ │ ├── proxy/ # Proxy - uses core.llm
│ │ ├── guard/ # Guard - uses core.llm
│ │ └── status/ # Status dashboard
│ │
│ ├── commands/ # Slash commands (installed to ~/.claude/commands)
│ ├── agents/ # Agents (installed to ~/.claude/agents)
│ └── skills/ # Skills (installed to ~/.claude/skills) — scripting layer (§5.5)
│
├── docs/
└── pyproject.toml
| Concern | Solution | Owner |
|---|---|---|
| Security isolation | Seatbelt/bubblewrap per-command | Claude Code native (sandbox-runtime) |
| Full container isolation | microVMs via docker sandbox run |
Docker Sandboxes |
| Proxy lifecycle coupling | --sidecar bundles proxy + Claude in Docker |
Forge sidecar mode |
Sidecar mode solves operational problems (not security): lifecycle coupling, port isolation, version consistency,
log isolation. Configurable via ~/.forge/config.yaml (proxy_mode: host|sidecar), overrideable with --sidecar /
--host-proxy. Mounts .claude/ and .forge/ from host; does NOT mount ~/.forge (UID issues, undermines port
isolation). Sidecar sessions also persist their launch mode, extra mounts, and image in intent.launch so
forge session resume <name> can replay the same runtime wiring later.
Forge still owns: Docker test infrastructure, runtime config. src/forge/sidecar/ provides sidecar mode —
operational, not a security sandbox.