Skip to content

Architecture

Fonn is an independent local application. It does not modify the source repository’s global Claude configuration. It is an npm-workspaces monorepo: packages/core (@nord-forge/fonn-core) owns reusable contracts, profiles, schemas, filtering, runtime; packages/cli (@nord-forge/fonn-cli) owns terminal input, rendering, and command dispatch. Public contracts live in packages/core/src/contracts.ts, profiles in packages/core/src/profiles.ts, and structured schemas in packages/core/src/schema.ts. The CLI imports the core package rather than copying its implementation.

The canonical built CLI entrypoint is packages/cli/dist/cli/main.js. Root dist/cli/main.js is a local development compatibility shim. The installed command is fonn: no arguments or leading launcher options (--cwd, --profile, --filter, --subagents, --state-dir, or --) open the native CLI selected by the profile’s provider. fonn claude and fonn codex explicitly choose a provider. Explicit SDK and administration commands remain separate; --help and --version are offline.

apps/desktop/README.md reserves a future desktop host of the reusable core. No desktop UI, .app, signing, notarization, or GUI cask is implemented. The current CLI cask is a separate delivery channel. See distribution.

The CLI parses and validates inputs, owns human-readable rendering and interactive approval, and delegates SDK execution to the runtime. Dispatch accepts injected I/O and runtime functions, so command tests need no live SDK. Help, profiles, version, and doctor stay offline.

The Claude launcher, available as fonn claude, spawns ordinary interactive Claude with inherited terminal I/O and per-launch profile/hook configuration. It keeps Claude’s normal authentication, policy, permission prompts, and question UI. It does not apply the SDK task budget, structured result, subagent limits, or accounting guarantees below. Passthrough options that could override the launch profile, settings hooks, or permission policy, as well as management/cloud subcommands, are rejected. The native profile is the initial selection; the ordinary interactive model/effort controls remain available. native-hook consumes one stdin JSON event and returns hook JSON; native-output recovers artifacts from the corresponding launch store. No global Claude settings file is automatically changed.

The runtime starts the SDK with the Claude Code system-prompt preset and preserves user, project, local, and managed settings/policies, including a custom CLAUDE_CONFIG_DIR. It uses normal permission handling, never bypass mode. Approval callbacks display untrusted requests and deny unless an interactive user explicitly answers yes. Automated/headless operation must work within the permissions already granted by project settings; the CLI does not silently approve new requests. SDK behavior is documented in the official permissions guide.

Profiles select a provider, model and effort level. The initial Claude profiles are: opus-high uses claude-opus-5-5 with high effort, opus-medium uses the same model with medium effort, and sonnet-medium uses claude-sonnet-5-5 with medium effort. Selection happens before a task starts and remains fixed for that task. Resume retains the saved profile and refuses incompatible changes. A task may stop because it completes, fails, is cancelled, reaches its turn limit, or exceeds its estimated budget; failure is never rendered as a successful check.

In SDK tasks, only the fixed explorer and reviewer read-only subagents are available. Each allows six turns with Read, Grep, and Glob; there are at most two foreground launches per task and no nested delegation. Explorer uses Sonnet with medium effort and reviewer uses Opus with medium effort. They cannot become arbitrary agent definitions or escalation routes. Their handoffs use a strict schema containing an agent name, task, findings, path/line evidence, and unresolved issues. The final task result is separately validated and records summary, changes, checks, unresolved items, and handoffs. Human rendering bounds handoff detail; show --json retains the complete validated result.

A PostToolUse integration sends each new observation to the output filter. Filtering has its own artifact store and mode. It returns the same shape plus a decision explaining what was delivered. It does not edit source files, previous messages, instructions, or reasoning. Unknown outputs and safety-relevant content remain unchanged. See filtering.

The default state root is ~/.local/state/fonn; --state-dir selects a different location. The state tree contains per-task records and tasks/<task-id>/artifacts/. Runtime helpers validate task IDs and confine artifact paths to that task’s store. Raw outputs may be sensitive and require manual retention. This is distinct from any SDK session storage used for resume. Resuming a task references its saved session and parent relationship; it is not arbitrary history rewriting.

Run records include status, timestamps, the fixed profile, target directory, budget and turn settings, estimated usage, filtering decisions, validated results, and errors. CLI JSON mode reserves stdout for a single JSON value. Progress and permission prompts use stderr. SIGINT aborts the runtime via an AbortSignal, and the CLI removes its signal listener after completion or failure.

Normal verification uses deterministic fixtures and mocked SDK messages. Live-model experiments require an explicit separate action and API budget. The official Agent SDK overview describes the underlying execution loop.

Native Ctrl+C handling respects the shared foreground process group: the terminal delivers the interrupt to Claude directly, so the wrapper does not forward it a second time. Without a TTY the wrapper forwards SIGINT. SIGTERM is forwarded. A signal sent only to the wrapper PID while it has a TTY is not forwarded as another SIGINT; programmatic callers should target the child or foreground process group.

SDK resumes use an atomic, owner-checked session lock and reject concurrent resumes. Cumulative usage regressions produce an explicit indeterminate-accounting failure. See the README for manual stale-lock recovery. Resume recovery searches only the current store and validated same-session/project ancestors, never arbitrary task directories.

The native Claude launch service currently belongs to the CLI workspace because it registers that CLI’s recovery hook entrypoint. A future desktop host can reuse the SDK core directly; sharing native launch services would require extracting that boundary.

Custom profiles and the selected default are stored separately from task state, in the Fonn configuration directory. Each profile contains only a name, optional provider (legacy profiles imply Claude), model and effort. SDK task records store the resolved profile snapshot; resume uses that saved value rather than the current registry, including after profile edits or deletion. Profile writes use a private atomic file and an exclusive mutation lock. See profiles.

Native Codex uses a separate launcher and post-tool hook adapter. Its stable hook definition is supplied per launch and remains subject to Codex’s normal trust and managed-policy checks. Codex profiles never enter the Claude SDK runtime. The codex:sol-medium built-in selects gpt-6.1-sol at medium effort; account availability is not verified. See Codex.

Native launches default to a saved subagents: auto preference. SessionStart adds concise orchestration guidance; SubagentStart adds filtering/recovery guidance when filtering is active. These hooks never grant tool permissions or start worker processes. The native providers apply session-wide PostToolUse hooks to supported child Bash observations, regardless of model. --subagents off removes automatic delegation guidance, while --filter off disables output reduction; both off installs no Fonn hooks. Native limits are guidance and provider settings, distinct from the enforced SDK bounds above. Legacy resume records without a subagent preference retain guidance off. See subagents.

Native launch state is shared by the Claude and Codex wrappers. Session lifecycle IDs are saved separately from launch metadata so an exit write cannot erase them. fonn resume replays a saved launch snapshot and uses the observed provider session ID, or an explicitly labeled native picker fallback when tracking is unavailable. It does not inspect provider transcript storage or guess the most recent session.