Commit Diff


commit - 0b367cd2488449362936e643c49ecfa371c49495
commit + d938d900f498a61d393c96bc79c0477cc5477209
blob - 8684989960ba557e858f510879a21bf60bdc7263
blob + ace7588809cd7bf4b32fd77282e8d3b2f1936db2
--- CONTEXT.md
+++ CONTEXT.md
@@ -7,10 +7,30 @@ vocabulary below is the project's ubiquitous language.
 
 ## Language
 
+### Human authority
+
+**Owner**:
+The one person who operates fugu and, in subscription mode, owns the Claude
+Pro or Max subscription consumed by every request. fugu is not shared with or
+operated on behalf of any other human.
+_Avoid_: user (when the human relationship matters), operator, customer, team
+
+**Invoking user**:
+The OS login identity of the Owner running this invocation. It selects legacy
+per-login policy and owns personal files; it is not separately configured as
+an account-routing or subscription identity.
+_Avoid_: end user, account
+
+**Administrator**:
+The root identity that installs fugu and its protected configuration. The
+Administrator and Owner may be the same person acting with different OS
+authority.
+_Avoid_: operator
+
 ### Conversation
 
 **Lead**:
-The conversation the user drives (as opposed to a subagent's).
+The conversation the Owner drives (as opposed to a subagent's).
 _Avoid_: main chat, primary conversation
 
 **Turn**:
@@ -80,7 +100,8 @@ _Avoid_: component, service
 
 **Custodian**:
 The single role holding a given credential after startup (I1);
-fugu-api for model keys, fugu-web for the search token.
+fugu-api for model keys or the Owner's OAuth token, fugu-web for the search
+token.
 
 **Coordinator**:
 `bin/fugu` — spawns workers, owns the Lead's Projection and Turn
@@ -105,7 +126,7 @@ hostile-content blast room), **fugu-tool** (tool execu
 role instance serves subagents), **fugu-editor** (compose spawn room).
 
 **Protect list**:
-Operator-configured paths unveiled away from every tool-executing
+Owner-configured paths unveiled away from every tool-executing
 role at startup; contents kernel-unreachable, ENOENT-shaped errors.
 
 ### Model-facing surface
@@ -120,21 +141,30 @@ An ephemeral, kernel-enforced read-only conversation s
 `agent` tool; reports text back, never persists, never nests (I15).
 
 **Palette**:
-The operator-configured set of models the `agent` tool's `model`
+The Owner-configured set of models the `agent` tool's `model`
 parameter may name (enumerated in its schema).
 
 **Skill**:
-A user-installed prompt template under `~/.fugu/skills/<name>/SKILL.md`,
-invocable as `/name` by the user or via the `skill` tool by the model.
+An Owner-installed prompt template under `~/.fugu/skills/<name>/SKILL.md`,
+invocable as `/name` by the Owner or via the `skill` tool by the model.
 
 **Project context**:
 `FUGU.md` (or `AGENTS.md`) plus personal `~/.fugu/FUGU.md`, injected
 into every projection; `#` lines append captures to the in-tree file.
 
 **Provider**:
-A wire protocol (anthropic or openai dialect) plus endpoint and key;
-named provider blocks coexist with the implicit default.
+A model route: the experimental Claude Subscription compatibility profile, or
+a supported direct anthropic/openai wire protocol plus endpoint and API key.
+Named provider blocks coexist with the implicit default.
 
+**Subscription adapter**:
+The versioned pure-C seam that converts fugu's canonical Projection to the
+currently observed Claude Code-compatible request shape, adds the Owner's
+Bearer token inside the Custodian, and maps Provider Tool names back to fugu's
+canonical names. It is unofficial, volatile, and personal-only; it is not a
+credential-sharing mechanism or an Anthropic-supported third-party API.
+_Avoid_: official SDK integration, stable OAuth API, Claude API provider
+
 **Search adapter**:
 The provider-specific request and response translation used by brokered
 web search; it owns no transport, credential, SSRF guard, or access policy.
@@ -149,6 +179,9 @@ unknown.
 
 - The **Coordinator** alone talks to every worker; **Workers** never
   talk to each other (authority map, I10).
+- The **Owner** is fugu's only human user. The **Invoking user** is that
+  person's OS identity for filesystem ownership and legacy match policy, not a
+  second domain participant or configurable subscription identity.
 - A **Turn** driven by the **Lead** belongs to a **Session**; only successful
   (or iteration-capped) Lead Turns survive in the **Journal**'s replay. A
   Turn run by a **Subagent** is ephemeral and never enters the Journal.
blob - /dev/null
blob + bda673d6d23f38f558b7765541d516402735e4ff (mode 644)
--- /dev/null
+++ .scratch/experimental-claude-subscription/spec.md
@@ -0,0 +1,445 @@
+# Experimental pure-C Claude subscription compatibility
+
+Status: ready-for-agent
+
+## Problem Statement
+
+The Owner wants fugu to consume the Claude Pro or Max subscription they
+already pay for because direct Anthropic API usage is prohibitively expensive.
+At the same time, fugu must remain an OpenBSD-native C program with no Node.js,
+Python, Claude Code executable, or Claude Agent SDK runtime dependency.
+
+The official JavaScript and Python Agent SDKs do not satisfy both constraints.
+Their open-source layers control a separate Claude Code executable; the
+subscription-authenticated HTTP behavior remains inside that executable. The
+transitional Node adapter currently being developed also widens the
+model-credential Custodian with filesystem and execution authority, adds a
+large ports dependency, and duplicates parts of fugu's existing agent loop.
+
+Third-party projects demonstrate that a Claude subscription credential can
+currently reach the Anthropic Messages service through a small set of observed
+Claude Code-compatible headers and body transformations. That protocol is
+undocumented, unsupported for third-party products, and liable to change or be
+blocked. fugu therefore needs a deliberately experimental implementation that
+preserves its two non-negotiable constraints without presenting the route as
+official or stable.
+
+## Solution
+
+Implement an experimental Claude Subscription adapter entirely in C. The
+Owner provisions a long-lived inference token with `claude setup-token` and
+stores it directly in fugu's protected configuration. The `claude` Provider route
+uses fugu's existing libtls HTTP state machine and Anthropic SSE decoder to
+send the canonical Projection directly to Anthropic after applying one small,
+versioned Claude Code-compatibility profile.
+
+The profile owns the volatile behavior: Bearer authentication and identifying
+headers, minimal subscription identity and billing-attribution entries,
+model-specific beta selection, relocation of fugu's instructions out of the
+validated system area, and reversible tool-name mapping. It contains no full
+copied Claude Code prompt and performs no browser login, token refresh, or
+credential discovery.
+
+The Owner remains fugu's only human participant by installation and release
+pattern; ownership is not duplicated as configuration or used for account
+routing. The credential remains solely in `fugu-api`, the existing
+model-credential Custodian. That role keeps its direct-provider steady state:
+TLS network access, no filesystem access, no writes, and no `execve(2)`.
+Direct Anthropic API-key and OpenAI-compatible Providers remain available as
+separate, supported routes.
+
+## User Stories
+
+1. As the Owner, I want to use my existing Claude Pro or Max subscription for Lead Generations, so that I do not incur direct API charges for ordinary personal use.
+2. As the Owner, I want subscription mode implemented entirely in C, so that fugu remains an OpenBSD-native program.
+3. As the Owner, I want subscription mode to require neither Node.js nor Python, so that I avoid large language-runtime dependencies.
+4. As the Owner, I want subscription mode to require neither Claude Code nor an Agent SDK installation, so that the package remains self-contained apart from base-system libraries.
+5. As the Owner, I want to supply a long-lived `claude setup-token` credential, so that fugu does not need to implement an interactive OAuth login.
+6. As the Owner, I want the token stored directly in fugu's already protected configuration, so that subscription setup has no redundant credential file.
+7. As the Owner, I want fugu to treat the token as opaque secret material, so that changes to token prefixes or lengths do not require needless parser changes.
+8. As the Owner, I want ownership inherent in the one-person release pattern, so that I do not configure a duplicate OS or account identity.
+9. As the Owner, I want every Claude subscription Provider to remain part of the same personal installation, so that named Provider blocks never imply account pooling or another human.
+10. As the Owner, I want subscription credentials forbidden in per-login match policy, so that configuration cannot turn my subscription into a shared local service.
+11. As the Owner, I want the subscription endpoint fixed to Anthropic over TLS, so that configuration cannot send my OAuth token to an arbitrary host.
+12. As the Owner, I want normal assistant text to stream through the existing interface, so that subscription and direct Providers feel the same during a Turn.
+13. As the Owner, I want fugu's existing tools to work with subscription models, so that I retain the same confined coding workflow.
+14. As the Owner, I want Claude's built-in tools excluded entirely, so that every model-selected action still passes through fugu's Tool policy and kernel confinement.
+15. As the Owner, I want tool calls mapped back to fugu's canonical names before execution, so that Provider-specific naming never leaks into Tool authorization.
+16. As the Owner, I want unknown or malformed subscription tool names rejected, so that a hostile Provider cannot smuggle an unoffered capability.
+17. As the Owner, I want multi-Generation Turns to preserve tool-use and tool-result relationships, so that the model can continue correctly after Tool execution.
+18. As the Owner, I want Steers and queued messages to retain their existing Turn semantics, so that selecting the subscription Provider does not change conversation behavior.
+19. As the Owner, I want Compaction to work through the subscription Provider, so that long Sessions remain usable.
+20. As the Owner, I want read-only Subagents to be able to use an explicitly selected subscription Provider, so that exploratory work can use the same subscription without gaining credentials or write authority.
+21. As the Owner, I want named subscription and direct Providers to coexist, so that I can switch routes explicitly with the existing model picker.
+22. As the Owner, I want the configured subscription model represented in the model picker even if OAuth model discovery is unavailable, so that route selection remains predictable.
+23. As the Owner, I want no silent fallback from subscription usage to a metered API key, so that an upstream compatibility failure cannot create unexpected charges.
+24. As the Owner, I want no silent fallback to another configured subscription account, so that credential ownership remains unambiguous.
+25. As the Owner, I want token usage and cache usage reported through the normal status and accounting surfaces, so that subscription Generations remain observable.
+26. As the Owner, I want quota-limit responses distinguished from invalid-token and compatibility-profile failures, so that I know what action is appropriate.
+27. As the Owner, I want transient connection, rate-limit, and server failures to follow fugu's existing bounded retry policy, so that subscription mode is not less resilient than direct mode.
+28. As the Owner, I want cancellation to terminate the in-flight HTTPS request without losing the last successful Turn, so that Session replay remains exact.
+29. As the Owner, I want failed subscription Turns excluded from the Journal transaction, so that resume reproduces only completed work.
+30. As the Owner, I want the OAuth token filtered from Provider text, tool fields, errors, logs, and journals, so that a hostile backend cannot reflect the credential out of its Custodian.
+31. As the Owner, I want dry-run configuration output to redact the token and identify the route as experimental, so that configuration inspection remains safe and honest.
+32. As the Owner, I want documentation and startup messaging to state that the route is unofficial and may stop working, so that I understand its maintenance and account-enforcement risks.
+33. As the Owner, I want manual token replacement to be sufficient when a token expires or is revoked, so that fugu never needs write access to credential storage.
+34. As the Administrator, I want the OpenBSD package to avoid Node, npm modules, Python, and opaque Claude executables, so that installation remains small and auditable.
+35. As the Administrator, I want subscription configuration to obey the same file-ownership, mode, and group checks as every other secret file, so that introducing OAuth does not weaken configuration secrecy.
+36. As a security reviewer, I want `fugu-api` to retain no-filesystem and no-exec steady-state promises, so that the original credential-custody architecture remains true.
+37. As a security reviewer, I want subscription response bytes parsed under the same bounds as hostile direct-API SSE, so that experimental authentication does not create a new unconfined parser.
+38. As a security reviewer, I want request identifiers generated locally and bounded, so that compatibility metadata cannot become a new ambient authority channel.
+39. As a maintainer, I want all volatile compatibility constants and transforms concentrated behind one adapter seam, so that an upstream change has one auditable repair location.
+40. As a maintainer, I want the active compatibility profile to have a visible identifier and validation date, so that bug reports can identify the request shape in use.
+41. As a maintainer, I want deterministic fixture tests for identity, attribution, beta, system, cache, and Tool transformations, so that profile updates are reviewable.
+42. As a maintainer, I want the complete credentialed Generation behavior exercised against a local TLS Provider fixture, so that tests cover the observable wire contract rather than private helper calls.
+43. As a maintainer, I want an explicit opt-in live check using the Owner's real token, so that upstream breakage can be detected before release without placing credentials in CI.
+44. As a maintainer, I want direct Anthropic and OpenAI-compatible regressions to remain unchanged, so that the experimental route cannot destabilize supported Providers.
+45. As a maintainer, I want an upstream rejection to fail closed with a bounded diagnostic naming the compatibility profile, so that stale behavior is visible rather than automatically disguised.
+46. As a maintainer, I want profile updates to require fresh fixtures and a recorded live result, so that copied folklore does not silently become production behavior.
+47. As the Owner, I want subscription mode limited to my personal interactive CLI use, so that fugu never suggests that hosting, account pooling, team use, or customer traffic is acceptable.
+48. As the Owner, I want direct API Providers retained, so that I have an officially supported alternative when subscription compatibility is unavailable.
+
+## Implementation Decisions
+
+- The two hard constraints are normative: the Claude Pro/Max subscription is
+  the source of inference quota, and the implementation is pure C with no
+  Node.js, Python, Claude Code executable, Agent SDK, embedded interpreter, or
+  ports runtime.
+
+- The existing `claude` Provider type remains the explicit configuration
+  spelling. It denotes the experimental Claude Subscription adapter. The
+  `anthropic` and `openai` Provider types retain their current direct-API
+  meanings and behavior.
+
+- Subscription mode accepts only an Owner-provisioned long-lived inference
+  token, normally produced by `claude setup-token`. fugu neither initiates
+  browser/device authorization nor reads Claude Code credential stores,
+  keychains, refresh tokens, or account lists.
+
+- fugu never refreshes or rewrites the token. Expiration, revocation, and
+  rotation are manual: the Owner replaces the inline value in the protected
+  configuration and starts a new invocation.
+
+- Owner identity is not configurable. The one-person installation and release
+  pattern is the subscription authorization boundary; the Invoking user's OS
+  identity remains relevant only to filesystem ownership and legacy match
+  policy. Subscription secrets remain prohibited in match blocks.
+
+- Subscription credentials use the existing secret parsing, file-descriptor
+  secrecy checks, bounded transfer, post-delegation wipe, redacted dump, and
+  hostile-response reflection filter. The token is NUL-, CR-, and LF-free but
+  otherwise opaque; the implementation does not make authorization decisions
+  from a cosmetic token prefix.
+
+- A `claude` Provider always connects to Anthropic's fixed production Messages
+  endpoint over port 443 with hostname verification. Endpoint overrides are
+  rejected for this Provider type rather than ignored. This prevents the
+  Owner's Bearer credential from being redirected to another host. Test
+  substitution exists only in regression builds or fixtures and is impossible
+  in an installed set-group-ID binary.
+
+- `fugu-api` remains the sole model-credential Custodian and sends the request
+  directly through its existing asynchronous libtls HTTP machinery. No new OS
+  process or worker Role is introduced.
+
+- `fugu-api` returns to the same steady-state promises as direct Anthropic
+  mode: standard I/O, Internet, and DNS only after loading the trust store. It
+  has no filesystem visibility, write authority, process creation, or
+  executable mappings. The Node/SDK exception is removed from the security
+  contract and dependency charter.
+
+- The transitional subprocess controller, JavaScript adapter, Node-specific
+  configuration, packaged SDK files, runtime dependency, protocol fields, and
+  extra pledge/unveil permissions are removed rather than retained as a second
+  implementation. `claude_node` becomes an invalid obsolete directive because
+  no released compatibility promise depends on it.
+
+- The Provider Generation boundary remains unchanged: the Coordinator sends a
+  bounded credential-free request body and Provider slot to `fugu-api`, and
+  receives the existing canonical text, Tool, usage, retry, completion, and
+  error events. Lead Turns, Subagent Turns, Compaction, cancellation, and model
+  switching therefore do not gain a second orchestration path.
+
+- One deep Subscription adapter owns the volatile compatibility profile. Its
+  credential-free half shapes the request body from fugu's canonical
+  Projection; its Custodian-side half adds credentialed headers and validates
+  or reverses Provider-specific response names. Common HTTP, TLS, SSE, Turn,
+  Journal, and Tool policy remain outside the adapter.
+
+- The compatibility profile is compile-time data, not Owner-configurable
+  impersonation knobs. It records a profile identifier, observed Claude Code
+  version and entrypoint, required beta features, identifying header values,
+  minimal identity text, billing-attribution algorithm, model exceptions, and
+  Tool-name convention. One profile is active in a release.
+
+- Each HTTPS request uses `Authorization: Bearer` with the Owner's token and
+  never sends `x-api-key`. The profile supplies the Anthropic protocol version,
+  OAuth and model beta values, CLI/application identity headers, bounded
+  per-Generation request identifier, and stable per-invocation Claude session
+  identifier required by the currently observed request shape.
+
+- Request and session identifiers are generated in C from base-system random
+  facilities. They carry no login name, project path, Session Journal id,
+  prompt text, or credential material.
+
+- The wire-level system array contains only the minimum compatibility identity
+  and billing-attribution entries demonstrated as necessary. fugu does not
+  vendor or reproduce Claude Code's full proprietary system prompt.
+
+- fugu's normal built-in instructions, configured system instructions, Project
+  context, and Subagent instructions remain part of the canonical Projection
+  and Journal semantics. At serialization time, the adapter relocates those
+  instructions to the leading user content required by the compatibility
+  profile while preserving their order and byte content. The transformation
+  is wire-only and never mutates the in-memory Projection or Journal.
+
+- Billing attribution is computed deterministically from the first applicable
+  Owner message and the pinned profile using OpenBSD base cryptographic
+  primitives. The billing entry is generated fresh, appears exactly once, and
+  is never accepted from Project context or model-controlled content.
+
+- Existing cache breakpoints are preserved where the compatibility request
+  shape permits them. The generated billing entry is not cache-marked, the
+  Provider's cache-control count is bounded, and moving fugu instructions does
+  not create duplicate cache markers.
+
+- Tool definitions and historical Tool-use blocks are mapped with one
+  deterministic, reversible profile convention. The mapping must be injective
+  for the offered Palette, bounded after expansion, and checked for collisions.
+  Tool ids and Tool inputs are not rewritten.
+
+- Subscription Tool names are syntactically reversed inside the hostile-stream
+  boundary before canonical Tool events reach the Coordinator. Unknown
+  prefixes, collisions, oversize names, and embedded NULs end the Generation
+  with a clean Provider error. The Coordinator's existing Generation-specific
+  Tool allowlist remains authoritative and rejects any canonical name that was
+  not offered before Tool dispatch.
+
+- fugu's canonical Projection already owns Tool-use/result consistency. The
+  Subscription adapter validates those relationships but does not silently
+  delete, synthesize, or repair Journal material to imitate third-party client
+  workarounds.
+
+- The existing Anthropic SSE decoder remains the response codec. The adapter
+  adds only the Provider-specific Tool-name reversal and any narrowly observed
+  error classification; all response byte, text, Tool argument, usage, total
+  time, and secret-reflection bounds remain in force.
+
+- The existing retry contract remains authoritative: bounded retries for
+  connection/TLS failures, HTTP 429, and pre-content 5xx responses; no retry
+  for invalid credentials, policy/compatibility rejection, malformed content,
+  or failures after assistant data has been accepted. `Retry-After` remains
+  bounded.
+
+- A stale profile produces a bounded error that identifies subscription mode
+  and the active profile without leaking request content or secrets. fugu does
+  not automatically probe alternative fingerprints, remove identity fields,
+  rotate accounts, or fall back to a billable API Provider.
+
+- OAuth model discovery is not assumed. A subscription Provider contributes
+  its configured model to the existing Palette; additional models require
+  additional configured Provider entries or a future separately specified
+  discovery mechanism.
+
+- Multiple named `claude` Provider entries may refer to the same Owner token
+  and different configured models, but account pooling, failover among human
+  accounts, quota aggregation, and traffic distribution are forbidden. Every
+  entry remains part of the same one-person installation.
+
+- The current request concurrency cap, per-request memory accounting,
+  backpressure, cancellation, total response deadline, and correlation-id
+  validation apply unchanged to subscription requests.
+
+- Configuration dumps, manuals, sample configuration, README, package
+  description, and domain language identify `claude` as experimental,
+  unofficial, personal-only, and liable to stop working or face upstream
+  enforcement. They do not call it an official SDK integration.
+
+- The domain definition of Subscription adapter changes from an SDK
+  stdin/stdout module to the narrow direct-C compatibility seam. The security
+  invariants and dependency charter remove their Node exception. ADR-0001's
+  original no-exec Custodian topology remains authoritative; a new ADR records
+  the decision to accept an undocumented compatibility profile and its
+  maintenance/legal consequences.
+
+- Direct API Providers remain the documented supported choice for hosting,
+  services, team or customer traffic, non-Owner use, and any situation that
+  needs an Anthropic-supported authentication contract.
+
+- Implementation proceeds as a replacement of the transitional Node work,
+  not as additive support for two subscription engines. Existing unrelated
+  worktree changes are preserved while the superseded adapter changes are
+  removed deliberately.
+
+## Testing Decisions
+
+- Tests assert externally observable behavior: accepted configuration,
+  emitted HTTPS request, canonical streamed events, Tool authorization,
+  terminal result, Journal outcome, and kernel-denied authority. They do not
+  assert private helper call order or internal struct layout.
+
+- The highest primary seam is one complete credentialed Generation through
+  the existing `fugu-api` Provider boundary. A local TLS Provider fixture
+  receives the exact headers and body, emits Anthropic SSE, and observes the
+  canonical events returned to the Coordinator.
+
+- The local TLS fixture is selected through regression-only wiring. Production
+  `claude` configuration cannot override Anthropic's hostname, port, or path,
+  and an installed set-group-ID binary ignores all test environment controls.
+
+- Contract fixtures cover the complete compatibility profile: Bearer rather
+  than API-key authentication, protocol and beta headers, generated request
+  and session identifiers, minimal identity entries, exactly one billing
+  attribution entry, relocation of fugu instructions, cache markers, model
+  exceptions, Tool definitions, historical Tool calls, and the absence of the
+  credential from the body.
+
+- Request fixtures use independently written minimal Projection examples and
+  public behavioral evidence. They do not copy a full Claude Code prompt or
+  snapshot unrelated third-party application content.
+
+- Tool mapping tests cover every built-in fugu Tool, round trips, underscore
+  names, leading case, maximum lengths, expansion overflow, duplicate mapped
+  names, unknown Provider names, unoffered names, and mappings split across
+  arbitrary SSE input chunks.
+
+- Projection tests prove that configured instructions, Project context,
+  Steers, Tool results, and Compaction instructions retain order and exact
+  bytes after wire relocation while the canonical Projection remains
+  unchanged.
+
+- Cache tests prove that billing is never cache-marked, generated identity
+  fields do not consume unintended cache slots, and normal prompt/tool/tail
+  breakpoints remain within the Provider's maximum.
+
+- Hostile-stream tests reuse the Anthropic codec's exhaustive chunk-split
+  style and add malformed mapped Tool names, NUL-bearing fields, oversize
+  identifiers, malformed JSON/SSE, missing terminals, reflected-token fields,
+  and data after a terminal event.
+
+- Error tests distinguish HTTP authentication failure, subscription policy or
+  fingerprint rejection, quota exhaustion, transient rate limiting, generic
+  5xx failure, malformed Provider errors, timeout, cancellation, and a stream
+  that ends after partial output.
+
+- Retry tests prove that no retry occurs after accepted assistant data and
+  that no failure silently selects an API-key Provider, another account, or an
+  alternate compatibility fingerprint.
+
+- Configuration regressions cover rejection of the obsolete `owner` directive,
+  named subscription Providers, forbidden match-block secrets, protected
+  inline tokens, redacted dumps, API-key/OAuth type mismatches, rejected subscription
+  endpoint overrides, and rejection of the obsolete Node directive.
+
+- Security regressions launch the real confined Custodian and attempt file
+  open, file creation, and process execution in subscription mode. The kernel
+  must deny them under the same steady pledge/unveil state as direct Anthropic
+  mode.
+
+- Secret tests place a distinctive fake token in every Provider-controlled
+  response location and failure path and prove that it appears in neither IPC,
+  terminal output, Journal records, diagnostics, nor logs.
+
+- Concurrency tests mix Lead, Subagent, direct-API, and subscription requests
+  through the existing request cap and verify independent correlation,
+  backpressure, cancellation, and teardown.
+
+- Existing `anthropic`, `http`, `conf`, `sandbox`, `turn`, `print`,
+  `turn_mechanics`, and `agentcfg` regression styles are the prior art. The
+  direct Anthropic and OpenAI fixture transcripts remain regression baselines.
+
+- A separate opt-in live check uses a real Owner-provided setup token and one
+  explicitly configured model. It verifies a minimal text Generation, one
+  harmless Tool call round trip, usage reporting, and credential
+  non-reflection. It never runs as root, never runs in ordinary CI, and never
+  prints or records the token.
+
+- A compatibility profile may be updated only when all hermetic tests pass and
+  a dated live check confirms the profile against Anthropic. The release notes
+  record the profile identifier and validation date without recording request
+  content or credentials.
+
+## Out of Scope
+
+- Reimplementing the Python or JavaScript Agent SDK's local NDJSON control
+  protocol.
+
+- Installing or launching Node.js, Python, Claude Code, an Agent SDK, Bun, or
+  another language runtime.
+
+- Interactive browser OAuth, PKCE/device login, refresh-token exchange,
+  keychain access, Claude credential-file discovery, or writing refreshed
+  credentials.
+
+- Supporting more than one human Owner, account pooling, quota aggregation,
+  automatic account rotation, team subscriptions, organization routing, or
+  sharing one person's subscription with another person.
+
+- Hosting fugu, exposing it as an API or daemon, or using the subscription
+  route for a product, service, public endpoint, team facility, or customer
+  traffic.
+
+- Guaranteeing that Anthropic permits, supports, or will continue accepting
+  the compatibility profile, or preventing Anthropic from limiting or
+  suspending the Owner's account.
+
+- Circumventing subscription quotas, rate limits, model entitlements, safety
+  controls, or account enforcement.
+
+- Copying or maintaining Claude Code's complete proprietary system prompt.
+
+- Automatically discovering new request fingerprints, trying a matrix of
+  client identities, or adapting silently when upstream validation changes.
+
+- OAuth model-list discovery. Subscription model choices remain explicitly
+  configured.
+
+- Changing the direct Anthropic Messages or OpenAI-compatible Provider
+  contracts beyond the refactoring required to share existing transport code.
+
+- Adding Linux, macOS, Windows, or general portability support.
+
+- Promising stable Journal replay across a future incompatible change to the
+  remote service; the Journal continues to preserve fugu's canonical
+  Projection, not Provider-specific wire bytes.
+
+## Further Notes
+
+The implementation is based on evidence, not an official third-party API
+contract. The strongest current positive reference is
+[`opencode-claude-auth`](https://github.com/griffinmartin/opencode-claude-auth),
+which has exercised direct subscription requests recently. The official
+[`claude-agent-sdk-python`](https://github.com/anthropics/claude-agent-sdk-python)
+source confirms that the Python SDK is only a local Claude Code subprocess
+controller and does not expose the upstream OAuth/HTTP implementation. Native
+Rust and Go attempts provide useful negative and enforcement evidence.
+
+Anthropic's authentication documentation supports setup tokens for Claude
+Code and SDK automation, but its direct API documentation does not describe
+subscription OAuth as third-party API authentication. Anthropic's legal and
+compliance guidance reserves subscription OAuth for Claude Code and native
+Anthropic applications and directs product developers to supported API or
+cloud authentication. The experimental label is therefore substantive, not a
+quality-stage euphemism.
+
+Suggested implementation sequence:
+
+1. Record the architectural decision and update domain/security/dependency
+   language to the pure-C Custodian design.
+2. Remove the transitional Node/Agent SDK surface and restore the package and
+   Custodian sandbox to the direct-provider baseline.
+3. Implement the credential-free compatibility profile, request shaping,
+   attribution, Tool mapping, and hermetic contract fixtures.
+4. Route `claude` through the existing direct TLS/SSE request lifecycle, add
+   response-name validation, and run the complete regression and hardened
+   sandbox gates.
+5. Run the explicit Owner credential live check, record the profile and date,
+   and ship the route with its experimental warning and supported direct-API
+   alternative.
+
+The feature is complete only when a stock OpenBSD package has no Node/Python/
+Claude runtime dependency, `fugu-api` demonstrably cannot open files or
+execute in subscription mode, a real setup token completes text and Tool
+Generations, the full hermetic and hardened suites pass, and every Owner-facing
+description consistently says one Owner and experimental subscription
+compatibility.
blob - bd7f5faab24a7828ea343f170255ed815be713c2
blob + 5072f52a8fbf1ce31be39436b8220b37823f6ac9
--- Makefile
+++ Makefile
@@ -6,8 +6,8 @@ SUBDIR=	src
 
 # The regress directory exists as a file, so the target must be
 # phony or make calls it up to date.
-.PHONY: regress obj cleandir check live-check live-check-privileged \
-	live-check-help
+.PHONY: regress obj cleandir check local-port live-check \
+	live-check-privileged live-check-help
 
 # Run every suite (charter.md).
 regress:
@@ -18,13 +18,15 @@ obj cleandir:
 	cd ${.CURDIR}/regress && ${MAKE} ${.TARGET}
 
 # Clean-room build from scratch followed by the full regress run;
-# the pre-commit gate (charter.md).
+# the pre-commit source gate (charter.md).
 check:
-	cd ${.CURDIR} && ${MAKE} cleandir
-	cd ${.CURDIR} && ${MAKE} obj
-	cd ${.CURDIR} && ${MAKE}
-	cd ${.CURDIR} && ${MAKE} regress
+	@cd ${.CURDIR} && exec ./scripts/check
 
+# Materialize the current clean commit as the checksummed local OpenBSD port.
+# Package construction and privileged installation remain ports targets.
+local-port:
+	cd ${.CURDIR} && exec ./scripts/local-port
+
 # Deliberately outside `check`: these spend real quota/reach the public
 # network, while the privileged half installs a package on a clean host.
 live-check:
blob - /dev/null
blob + 5b7f6456e2bd4a8183f26593fdbcb59748072d20 (mode 644)
--- /dev/null
+++ AGENTS.md
@@ -0,0 +1,13 @@
+## Agent skills
+
+### Issue tracker
+
+Issues are tracked as local Markdown under `.scratch/<feature-slug>/`. See `docs/agents/issue-tracker.md`.
+
+### Triage labels
+
+Use the five default canonical triage label strings. See `docs/agents/triage-labels.md`.
+
+### Domain docs
+
+This repo uses the single-context layout. See `docs/agents/domain.md`.
blob - 63d2a1eb87372b59bb89ec8baa9333bcb31a0012
blob + c6cc80ceda7964ad32c64531341ac64943aedbf9
--- README
+++ README
@@ -1,17 +1,54 @@
 fugu — an OpenBSD-native AI coding agent
 ========================================
 
-fugu is an interactive terminal program that pairs a large-language-
-model chat API with local tools (file read/write/edit, shell, search,
+fugu is a personal interactive terminal program for one **Owner**: the human
+who runs it and owns every model credential it consumes. It pairs a large-language-
+model with local tools (file read/write/edit, shell, search,
 brokered web access, and bounded read-only subagents) so a model can
-work inside a project under the user's direction. It is
+work inside a project under the Owner's direction. It is
 privilege-separated in the manner of OpenBSD
 base daemons: every security property is enforced by pledge(2),
 unveil(2), and process boundaries, never by prompt or convention.
 
+The experimental Claude subscription adapter is deliberately single-person.
+It uses the Owner's Claude Pro or Max quota through an unofficial, observed
+Claude Code-compatible HTTP profile implemented entirely in C. It is not an
+Anthropic-supported third-party authentication contract; it may stop working
+without notice or trigger upstream account enforcement. Do not host fugu,
+share it with another human, put it behind a service or API, or use one
+subscription token for team traffic. Direct API providers are the supported
+route for every shared, service, team, or customer-facing use.
+
 fugu is OpenBSD-only, by design. There are no portability shims; a
 build failing on any other system is correct behavior.
 
+Claude Max setup
+----------------
+
+Generate a long-lived inference token while logged into the Owner's Claude Pro
+or Max account:
+
+	claude setup-token
+
+Paste the printed token into the protected configuration, then select the
+personal subscription Provider:
+
+	provider "claude"
+	model "claude-sonnet-4-6"
+	oauth_token "sk-ant-oat01-..."
+
+The `claude` command is used only to provision the token; fugu neither installs
+nor executes Claude Code. At runtime the token exists only in `fugu-api`, which
+adds it to a fixed Anthropic TLS request after the process has lost all file,
+write, and execution authority. Every model-selected Tool still passes through
+fugu's existing allowlist and kernel-confined executors.
+
+The package has no Node.js, Python, Claude Code, or Agent SDK runtime
+dependency. The active compatibility profile is `claude-code-2.1.185`; its
+snapshot date is 2026-07-16. Its identifying headers and request transforms are
+intentionally versioned because they are undocumented and volatile. Token
+refresh and replacement are manual.
+
 Lineage
 -------
 
@@ -33,7 +70,7 @@ Clone it with an explicit destination when creating th
 
 	git clone ssh://got.fugu.farm/fugu_agent.git /home/isaac/fugu2
 
-The current port version is 0.2.  The `v0.1` tag preserves the previous
+The current port version is 0.3.  The `v0.1` tag preserves the previous
 canonical release state.
 
 Building
@@ -45,8 +82,18 @@ BSD make on OpenBSD-current or the current release:
 	make
 	make regress	# run every test suite
 	make check	# clean-room rebuild + full regress (pre-commit gate)
+	make VERBOSE=1 check	# same gate with the complete live transcript
+	make local-port	# stage clean HEAD as a checksummed local port
 	make install	# PREFIX defaults to /usr/local
 
+The default `make check` transcript reports concise phase and per-suite status,
+check counts, and total elapsed time. It fails immediately on a compiler
+warning, failed test, skipped/disabled proof, or expected/unexpected failure,
+then prints the complete failed output and a focused rerun command. Only
+`VERBOSE=1` enables live compiler commands and individual assertions; other
+nonempty values are rejected. `make regress` retains the ordinary detailed
+OpenBSD regression output.
+
 A hardened debug build compiles with UBSan in trap mode:
 
 	make FUGU_DEBUG=1 check
@@ -73,7 +120,10 @@ may override `BINOWN` and `BINGRP` together with `DEST
 
 For ordinary local deployment and package-level testing, build and install
 through the OpenBSD ports framework rather than running the source-tree
-install target as root.  The complete checkout-to-package procedure,
+install target as root. `make local-port` creates the committed source archive,
+stages `productivity/fugu` under `/usr/ports/mystuff`, and runs the framework's
+checksum targets; package construction and installation remain explicit ports
+operations. The complete checkout-to-package procedure,
 including the local/remote repository topology, SSH agent forwarding,
 deployment verification, rollback, and repeat installations during
 development, is in
blob - /dev/null
blob + 8a4b206523cc084442030425e39315c968df4f82 (mode 644)
--- /dev/null
+++ docs/adr/0007-experimental-claude-subscription-compatibility.md
@@ -0,0 +1,89 @@
+# Experimental pure-C Claude subscription compatibility
+
+Status: accepted, 2026-07-16
+
+## Context
+
+The Owner wants ordinary personal fugu Generations to consume the Claude Pro
+or Max subscription they already pay for. Direct Anthropic API usage is a
+supported contract but is prohibitively expensive for this workflow. The
+official JavaScript and Python Agent SDKs do not solve the native-client
+problem: their open-source layers launch a separate Claude Code executable,
+and adding either SDK would introduce a language runtime while widening the
+model-credential Custodian with filesystem and execution authority.
+
+Independent clients show that a subscription setup token currently works with
+an observed Claude Code-compatible Messages request. Anthropic does not
+document or support that route as third-party product authentication. It can
+change or be blocked, and its use may result in account enforcement.
+
+## Decision
+
+The `claude` Provider is an explicitly experimental, Owner-only compatibility
+route implemented entirely in C. The active profile is
+`claude-code-2.1.185`, snapshot 2026-07-16.
+
+The Coordinator builds a credential-free Provider view from fugu's canonical
+Projection. One small adapter owns the volatile identity, billing attribution,
+beta selection, cache placement, instruction relocation, and reversible Tool
+name mapping. `fugu-api` remains the only credential Custodian, adds the
+Bearer token and identifying headers, and sends the request through its
+existing libtls and Anthropic SSE machinery to a fixed Anthropic endpoint.
+
+The Owner provisions a long-lived inference token manually, normally with
+`claude setup-token`, and stores it directly in the protected fugu
+configuration. fugu does not perform browser OAuth, discover credential
+stores, refresh or rewrite tokens, execute Claude Code, or include Node.js,
+Python, or an Agent SDK.
+
+The Owner is inherent in fugu's personal installation and invocation pattern;
+it is not duplicated as configuration or used for account routing. The route
+is for that one Owner's interactive CLI use only. It is not permitted for
+hosting, services, teams, customers, account pooling, quota aggregation, or
+another human's traffic. Subscription secrets remain forbidden in per-login
+match policy.
+
+Profile failures fail closed and name the active profile. fugu does not probe
+alternate fingerprints, rotate accounts, evade quota or safety controls, or
+fall back silently to a metered API Provider. Direct Anthropic and
+OpenAI-compatible Providers remain the supported alternative.
+
+## Security consequences
+
+The original ADR-0001 Custodian boundary remains intact. After loading its CA
+trust store, `fugu-api` keeps only `stdio inet dns`: no file opens, writes, or
+`execve(2)`. Subscription response bytes use the same hostile-stream bounds,
+secret-reflection filter, retry policy, cancellation, correlation, and memory
+accounting as direct Anthropic responses. Provider Tool names are normalized
+inside that boundary and then checked against the Generation's offered Palette
+before Tool dispatch.
+
+The production endpoint is not configurable, preventing redirection of the
+Owner's token. A local TLS substitution exists solely for regressions and is
+ignored by set-group-ID execution; the Custodian independently validates it.
+
+## Maintenance consequences
+
+The compatibility profile is compile-time data rather than configuration.
+Updating it requires deterministic fixtures, the full hermetic and hardened
+regression gates, and an explicit ordinary-Owner live check. A release record
+may state the profile and validation date but must never retain credentials or
+request content.
+
+The implementation and documentation must continue to call this route
+unofficial, volatile, and experimental. Research evidence and limitations are
+recorded in `docs/research/claude-subscription-native-clients.md` and
+`docs/research/claude-python-sdk-reverse-engineering.md`.
+
+## Rejected alternatives
+
+- The JavaScript Agent SDK and Node.js: large runtime dependency and an
+  execution/filesystem exception in the credential Custodian.
+- The Python Agent SDK: also a Claude Code subprocess controller, not a native
+  OAuth/HTTP implementation.
+- Executing Claude Code directly: opaque runtime behavior and duplicated agent
+  orchestration.
+- Direct API only: supported and retained, but does not meet the Owner's cost
+  constraint.
+- Multiple subscription engines or automatic fingerprint discovery: larger
+  attack surface and behavior too close to bypassing upstream enforcement.
blob - b72b639bd6ac77e55ca61797dbf54ab77a418032
blob + bc52461e2f27a308cb13af3f73f3e345053213db
--- docs/design/m10-release.md
+++ docs/design/m10-release.md
@@ -3,7 +3,7 @@
 Status: implementation and integrated hermetic verification complete,
 2026-07-11.  Clean-room normal and `FUGU_DEBUG=1` gates both pass all 28
 OpenBSD suites with no warnings, sanitizer diagnostics, or failures.
-Credentialed and privileged operator checks remain outstanding as documented
+Credentialed and privileged manual checks remain outstanding as documented
 in `docs/release-live-checks.md`.
 
 There is no normative document that assigns a complete feature bundle to
@@ -191,7 +191,7 @@ Release checks cover:
 
 Credentialed and privileged live checks are never silently replaced by a
 stub.  If they are not run in the development environment, the handoff names
-them explicitly as outstanding operator checks.
+them explicitly as outstanding manual checks.
 
 The hardened build disables only Clang's function-type sanitizer.  Its
 metadata probe reads executable text, which OpenBSD maps execute-only, and
blob - 7fcbb3a23b7e972c5f3673034636095c72de2c90
blob + e8324d9fadbfe9d0d87233da03609c5638756493
--- docs/design/m3-privsep.md
+++ docs/design/m3-privsep.md
@@ -43,7 +43,7 @@ Each worker's coordinator channel is one `socketpair(2
 `PROC_PARENT_SOCK_FILENO` idiom). The coordinator holds the other
 end and an `imsgbuf` per worker.
 
-The terminal: the coordinator is what the user launched, so it owns
+The terminal: the coordinator is what the Owner launched, so it owns
 the tty on 0/1/2. It passes those three fds to **fugu-tty** (dup2 in
 the child before exec); the coordinator then points its own 0/1/2 at
 `/dev/null` except stderr, which it keeps for `err(3)` startup
blob - 4efeb305561bc95823de018fbeb5f11f76438151
blob + f1131e54f65e11712db66b49a5c136c7076973ac
--- docs/design/m6-line-and-context.md
+++ docs/design/m6-line-and-context.md
@@ -74,7 +74,7 @@ an optional `---`-fenced header (`name`, `description`
 header `name` when valid (alnum/`-`/`_`), else the directory name. A
 file over 256 KiB, NUL-bearing, with an unterminated header, or with
 an empty body is skipped; at most 256 load. `/name [text]` fires the
-body as the user's message with any trailing text as a second
+body as the Owner's message with any trailing text as a second
 paragraph. Unless disabled, the skill is also offered as the `skill`
 tool, whose input schema enumerates the valid names and whose result
 (resolved by the coordinator, never leaving the process) is the body.
blob - d5c7cf4ed525077bc1c6147058bc3be51cd8c15d
blob + 4763e2d1c582f79c0fc3fdf32bea3a3e753638cd
--- docs/design/m7-curses.md
+++ docs/design/m7-curses.md
@@ -126,7 +126,7 @@ file, `exec`s the editor on the shared terminal, reads
 `EDIT_RESULT{bytes}`; the coordinator relays it to fugu-tty, which
 `refresh`es and replaces the input line. A nonzero editor exit leaves
 the input untouched. fugu-editor's steady pledge is
-`stdio rpath wpath cpath tty proc exec`; it parses only the user's own
+`stdio rpath wpath cpath tty proc exec`; it parses only the Owner's own
 seed and the file it wrote, never model or provider bytes.
 
 ## The drawer and session switching
@@ -176,7 +176,7 @@ The remaining §2.1 surfaces landed in the M7 follow-u
 
 - **fugu-editor** (the `v` compose key): the fifth worker, spawned
   `pass_tty`, steady pledge `stdio rpath wpath cpath tty proc exec`
-  with no execpromises so the user's editor runs unpledged (the
+  with no execpromises so the Owner's editor runs unpledged (the
   `tmppath` promise is gone from pledge(2), so the file promises stay
   whole-filesystem).  The seed and result relay through the
   coordinator in metered chunks; fugu-tty suspends curses and stops
blob - 7d9db4c7b455643ddce1749314ba82031f979427
blob + d866fca296b4f113727ab29c4ee5f8efdabe6987
--- docs/design/m9-subagents.md
+++ docs/design/m9-subagents.md
@@ -2,7 +2,7 @@
 
 Status: implementation and verification complete, 2026-07-11.
 The OpenBSD `regress/turn` agent target passes 126 checks.  This slice adds the
-model-facing `agent` tool, its operator-bounded route palette, fresh read-only
+model-facing `agent` tool, its Owner-bounded route palette, fresh read-only
 workers, bounded execution, and the lead-only journal boundary required by
 behavior.md §5.12 and invariant I15.  The combined normal and hardened release
 gates also pass as recorded by `docs/design/m10-release.md`.
@@ -65,7 +65,7 @@ attempt.  This stays true even when the lead has `allo
 `allow_subprocess_net yes`.  The local tool definitions are exactly `read`,
 `grep`, `find`, `ls`, and `shell`.
 `web_search`/`web_fetch` and allowlisted `http_request` are added only under the
-same operator gates as the lead and remain brokered through `fugu-web`.
+same Owner gates as the lead and remain brokered through `fugu-web`.
 Mutation, background jobs, skills, and `agent` itself are absent; an unexpected
 request for any of them is rejected as unavailable as a second line of
 defense.
blob - /dev/null
blob + 831e1c37cb1fef6680655cdd13539c06055c500e (mode 644)
--- /dev/null
+++ docs/agents/domain.md
@@ -0,0 +1,53 @@
+# Domain Docs
+
+How the engineering skills should consume this repo's domain documentation when exploring the codebase.
+
+This repository uses the single-context layout: `CONTEXT.md` and `docs/adr/` live at the repository root.
+
+## Before exploring, read these
+
+- **`CONTEXT.md`** at the repo root, or
+- **`CONTEXT-MAP.md`** at the repo root if it exists — it points at one `CONTEXT.md` per context. Read each one relevant to the topic.
+- **`docs/adr/`** — read ADRs that touch the area you're about to work in. In multi-context repos, also check `src/<context>/docs/adr/` for context-scoped decisions.
+
+If any of these files don't exist, **proceed silently**. Don't flag their absence; don't suggest creating them upfront. The `/domain-modeling` skill (reached via `/grill-with-docs` and `/improve-codebase-architecture`) creates them lazily when terms or decisions actually get resolved.
+
+## File structure
+
+Single-context repo (most repos):
+
+```
+/
+├── CONTEXT.md
+├── docs/adr/
+│   ├── 0001-event-sourced-orders.md
+│   └── 0002-postgres-for-write-model.md
+└── src/
+```
+
+Multi-context repo (presence of `CONTEXT-MAP.md` at the root):
+
+```
+/
+├── CONTEXT-MAP.md
+├── docs/adr/                          ← system-wide decisions
+└── src/
+    ├── ordering/
+    │   ├── CONTEXT.md
+    │   └── docs/adr/                  ← context-specific decisions
+    └── billing/
+        ├── CONTEXT.md
+        └── docs/adr/
+```
+
+## Use the glossary's vocabulary
+
+When your output names a domain concept (in an issue title, a refactor proposal, a hypothesis, a test name), use the term as defined in `CONTEXT.md`. Don't drift to synonyms the glossary explicitly avoids.
+
+If the concept you need isn't in the glossary yet, that's a signal — either you're inventing language the project doesn't use (reconsider) or there's a real gap (note it for `/domain-modeling`).
+
+## Flag ADR conflicts
+
+If your output contradicts an existing ADR, surface it explicitly rather than silently overriding:
+
+> _Contradicts ADR-0007 (event-sourced orders) — but worth reopening because…_
blob - /dev/null
blob + 6b124b36719c01a9b1d5bbdfa1c0352286105794 (mode 644)
--- /dev/null
+++ docs/agents/issue-tracker.md
@@ -0,0 +1,30 @@
+# Issue tracker: Local Markdown
+
+Issues and specs (you may know a spec as a PRD) for this repo live as markdown files in `.scratch/`.
+
+## Conventions
+
+- One feature per directory: `.scratch/<feature-slug>/`
+- The spec is `.scratch/<feature-slug>/spec.md`
+- Implementation issues are one file per ticket at `.scratch/<feature-slug>/issues/<NN>-<slug>.md`, numbered from `01` — never a single combined tickets file
+- Triage state is recorded as a `Status:` line near the top of each issue file (see `triage-labels.md` for the role strings)
+- Comments and conversation history append to the bottom of the file under a `## Comments` heading
+
+## When a skill says "publish to the issue tracker"
+
+Create a new file under `.scratch/<feature-slug>/` (creating the directory if needed).
+
+## When a skill says "fetch the relevant ticket"
+
+Read the file at the referenced path. The user will normally pass the path or the issue number directly.
+
+## Wayfinding operations
+
+Used by `/wayfinder`. The **map** is a file with one **child** file per ticket.
+
+- **Map**: `.scratch/<effort>/map.md` — the Notes / Decisions-so-far / Fog body.
+- **Child ticket**: `.scratch/<effort>/issues/NN-<slug>.md`, numbered from `01`, with the question in the body. A `Type:` line records the ticket type (`research`/`prototype`/`grilling`/`task`); a `Status:` line records `claimed`/`resolved`.
+- **Blocking**: a `Blocked by: NN, NN` line near the top. A ticket is unblocked when every file it lists is `resolved`.
+- **Frontier**: scan `.scratch/<effort>/issues/` for files that are open, unblocked, and unclaimed; first by number wins.
+- **Claim**: set `Status: claimed` and save before any work.
+- **Resolve**: append the answer under an `## Answer` heading, set `Status: resolved`, then append a context pointer (gist + link) to the map's Decisions-so-far in `map.md`.
blob - /dev/null
blob + b716855d485f3865f9dfde2a82141721f065b2e7 (mode 644)
--- /dev/null
+++ docs/agents/triage-labels.md
@@ -0,0 +1,15 @@
+# Triage Labels
+
+The skills speak in terms of five canonical triage roles. This file maps those roles to the actual label strings used in this repo's issue tracker.
+
+| Label in mattpocock/skills | Label in our tracker | Meaning                                  |
+| -------------------------- | -------------------- | ---------------------------------------- |
+| `needs-triage`             | `needs-triage`       | Maintainer needs to evaluate this issue  |
+| `needs-info`               | `needs-info`         | Waiting on reporter for more information |
+| `ready-for-agent`          | `ready-for-agent`    | Fully specified, ready for an AFK agent  |
+| `ready-for-human`          | `ready-for-human`    | Requires human implementation            |
+| `wontfix`                  | `wontfix`            | Will not be actioned                     |
+
+When a skill mentions a role (e.g. "apply the AFK-ready triage label"), use the corresponding label string from this table.
+
+Edit the right-hand column to match whatever vocabulary you actually use.
blob - 5c2f163d2af2e47e7e50709a15cf7176c7ff298d
blob + a51b8fb17ec60e7d0331f4e5096656af7857cc4a
--- docs/openbsd-development.md
+++ docs/openbsd-development.md
@@ -7,7 +7,7 @@ ports framework creates and installs the package that 
 ownership, modes, manuals, and the sample configuration.
 
 The canonical repository is `ssh://got.fugu.farm/fugu_agent.git`.  The current
-port version is 0.2.
+port version is 0.3.
 
 ## Repository and deployment topology
 
@@ -114,49 +114,38 @@ make
 make regress
 ```
 
-## Create a local distfile and port
+## Prepare the local port
 
-Package a clean committed tree.  `git archive` deliberately excludes `.git`,
-untracked files, build objects, and uncommitted changes.
+Materialize the current clean commit as a checksummed local port:
 
 ```sh
 cd /home/isaac/fugu2
-git status --short
-
-V=0.2
-git archive --format=tar.gz --prefix="fugu-${V}/" \
-    -o "/tmp/fugu-${V}.tar.gz" HEAD
-doas install -m 0644 "/tmp/fugu-${V}.tar.gz" \
-    "/usr/ports/distfiles/fugu-${V}.tar.gz"
+make local-port
 ```
 
-`git status --short` must print nothing before continuing.
+`make local-port` refuses a dirty working tree, stages the maintained port at
+`/usr/ports/mystuff/productivity/fugu`, and asks the ports framework for its
+computed package path, package name, distfile name, and repository paths. It
+then archives the exact `HEAD` commit with the computed `DISTNAME` prefix,
+installs that archive in the computed distfile directory, and runs both
+`makesum` and `checksum`. The old staged port is restored if validation fails.
 
-Stage the port under `mystuff`, the location recommended for testing local or
-uncommitted ports.  `mystuff` is an alternate ports root, so retain the normal
-`<category>/<port>` structure beneath it; this keeps the package path
-`productivity/fugu`.  This copy is disposable; the maintained source remains
-in `port/productivity/fugu/` in the repository.
+`git archive` deliberately excludes `.git`, build objects, untracked files,
+and uncommitted changes. The generated `distinfo` belongs only to the
+disposable staged port; the maintained source remains in
+`port/productivity/fugu/`. `mystuff` retains the normal
+`<category>/<port>` structure, so the package path remains
+`productivity/fugu`.
 
-```sh
-cd /home/isaac/fugu2
-ME=$(id -un)
-GROUP=$(id -gn)
-PORT=/usr/ports/mystuff/productivity/fugu
+Build the resulting package through the ports framework:
 
-doas install -d -o "$ME" -g "$GROUP" "${PORT%/fugu}"
-rm -rf "$PORT"
-cp -R port/productivity/fugu "$PORT"
-
-cd "$PORT"
-make makesum
-make package
+```sh
+make -C /usr/ports/mystuff/productivity/fugu package
 ```
 
-`make makesum` creates the deliberately untracked `distinfo` from the exact
-archive.  `make package` performs the fetch/checksum, clean extraction, build,
-fake installation, packing-list, and package creation stages.  Do not copy
-files directly into `/usr/local`; the package is the artifact under test.
+`make package` performs the fetch/checksum, clean extraction, build, fake
+installation, packing-list, and package creation stages. Do not copy files
+directly into `/usr/local`; the package is the artifact under test.
 
 ## Deploy the package on the server
 
@@ -165,8 +154,7 @@ its `/home/isaac/fugu2` checkout.  Install a newly bui
 ports framework:
 
 ```sh
-cd /usr/ports/mystuff/productivity/fugu
-doas make install
+doas make -C /usr/ports/mystuff/productivity/fugu install
 ```
 
 For another build of the same unpublished version, clear this local port's
@@ -174,10 +162,11 @@ work directory, package artifact, and registered test 
 force a package-level reinstall:
 
 ```sh
-cd /usr/ports/mystuff/productivity/fugu
-make clean=all
-make package
-doas make reinstall
+cd /home/isaac/fugu2
+make local-port
+make -C /usr/ports/mystuff/productivity/fugu clean=all
+make -C /usr/ports/mystuff/productivity/fugu package
+doas make -C /usr/ports/mystuff/productivity/fugu reinstall
 ```
 
 `make clean=all` includes `clean=plist`.  That is appropriate only while
@@ -191,7 +180,7 @@ The canonical direct `pkg_add` equivalent for an unsig
 package uses a trusted package directory, not a `file:` URL:
 
 ```sh
-doas env TRUSTED_PKG_PATH=/usr/ports/packages/%a/all/ pkg_add fugu-0.2
+doas env TRUSTED_PKG_PATH=/usr/ports/packages/%a/all/ pkg_add fugu-0.3
 ```
 
 Normally prefer `make install` or `make reinstall`; the ports framework supplies
@@ -206,9 +195,10 @@ pkg_info -e 'fugu-*'
 ls -l /usr/local/bin/fugu
 ```
 
-To run fugu, create the real configuration from the installed sample, edit in
-the provider, model, and key-file path, and protect both configuration and key
-files from other users:
+To run fugu, create the real configuration from the installed sample and edit
+in the provider and model. Store a Claude subscription `oauth_token` directly
+in this protected configuration; direct API and search credentials may still
+use separate key files:
 
 ```sh
 doas cp /etc/fugu.conf.sample /etc/fugu.conf
@@ -216,7 +206,7 @@ doas chown root:_fugu /etc/fugu.conf
 doas chmod 0640 /etc/fugu.conf
 doas vi /etc/fugu.conf
 
-# Apply the same owner, group, and mode to every configured key file.
+# Apply the same owner, group, and mode to any configured key file.
 doas chown root:_fugu /etc/fugu.key
 doas chmod 0640 /etc/fugu.key
 fugu -n
blob - fcfbdd55ec7669f7f8df00ca1e87697faf29c527
blob + fe5cb8ab135cc30f6edfe3f9605dafec8da07cb5
--- docs/release-live-checks.md
+++ docs/release-live-checks.md
@@ -1,6 +1,6 @@
 # M10 live release checks
 
-Status: automated operator checks, not yet run against the release package.
+Status: automated manual checks, not yet run against the release package.
 The integrated hermetic normal and hardened gates passed on 2026-07-11 as
 recorded in `docs/design/m10-release.md`.  These checks remain outside
 `make check` because they spend real provider/search quota, reach the public
@@ -20,7 +20,9 @@ Prepare the installed `/etc/fugu.conf` and its key fil
 - `max_subagents` is at least 2, and `subagent_provider` names a real provider
   block with `subagent_model` set;
 - `web_search yes` and a real Kagi key are configured;
-- provider and Kagi credentials live in separate key files, not inline.
+- direct provider and Kagi credentials live in separate key files;
+- a Claude subscription `oauth_token` lives directly in the protected
+  configuration.
 
 As an ordinary user, run:
 
@@ -29,6 +31,25 @@ env FUGU_LIVE_SECRET_FILES='/etc/fugu.key /etc/fugu-wo
     make live-check
 ```
 
+To validate the experimental Claude subscription profile in a one-Owner
+installation, make `claude` the active route and store its setup token directly
+as `oauth_token` in `/etc/fugu.conf`:
+
+```sh
+env FUGU_LIVE_EXPECT_PROVIDER=claude \
+    FUGU_LIVE_EXPECT_MODEL=claude-sonnet-4-6 \
+    FUGU_LIVE_SECRET_FILES='/etc/fugu-work.key /etc/fugu-kagi.key' \
+    make live-check
+```
+
+That opt-in run adds a harmless one-read Tool round, checks the experimental
+startup warning and redacted profile identifier, and securely extracts the
+inline token only inside the leak-scanning subprocess. It subjects the token
+to the same output, Journal, and syslog non-reflection scans without displaying
+or copying it into an ordinary-user file. It must run as
+the ordinary Owner account, never root. Record the date and profile identifier
+in the release transcript; do not record request content or the token.
+
 The key-file list is mandatory.  Each file must contain exactly one nonempty
 printable key line, which the target uses as a fixed pattern when checking
 stdout, stderr, the isolated session journals, and a uniquely delimited syslog
@@ -65,19 +86,20 @@ and `FUGU_LIVE_LIBEXEC`.  Run `make live-check-help` f
 ## Clean package and set-group-ID target
 
 Use a disposable, clean OpenBSD 7.9 host where `_fugu`, the installed binary,
-and `/etc/fugu.conf` do not already exist.  Prepare a self-contained,
-root-owned mode-0600 configuration source that uses only key-file directives;
-inline secret directives, `include`, and macros are rejected so the audit
-cannot hide or miss another secret-bearing directive.  Put its root-owned
-mode-0600 key files at their final paths; the target changes them to
+and `/etc/fugu.conf` do not already exist. Prepare a self-contained,
+root-owned mode-0600 configuration source. Direct API and Kagi credentials use
+key-file directives, while a Claude subscription token uses an inline
+`oauth_token`; other inline secret directives, `include`, and macros are
+rejected so the audit cannot hide or miss another secret-bearing directive.
+Put its root-owned mode-0600 key files at their final paths; the target changes them to
 `root:_fugu` mode 0640 after the package creates the group.  Choose an ordinary
 test user who is not a member of `_fugu`, then run as root:
 
 ```sh
-doas env FUGU_LIVE_PACKAGE=/tmp/fugu-0.2.tgz \
+doas env FUGU_LIVE_PACKAGE=/tmp/fugu-0.3.tgz \
     FUGU_LIVE_CONF_SOURCE=/root/fugu-live.conf \
     FUGU_LIVE_SECRET_FILES='/etc/fugu.key /etc/fugu-work.key /etc/fugu-kagi.key' \
-    FUGU_LIVE_CONFIRM="$(hostname):$(sha256 -q /tmp/fugu-0.2.tgz)" \
+    FUGU_LIVE_CONFIRM="$(hostname):$(sha256 -q /tmp/fugu-0.3.tgz)" \
     FUGU_LIVE_USER=alice make live-check-privileged
 ```
 
blob - /dev/null
blob + 8fa85420723863c21e484377f3d0a5c6e4589c7f (mode 644)
--- /dev/null
+++ docs/research/claude-python-sdk-reverse-engineering.md
@@ -0,0 +1,92 @@
+# Claude Python SDK: source-level reverse-engineering value
+
+Research date: 2026-07-16
+
+Source inspected: Anthropic's `claude-agent-sdk-python` at commit
+[`347a1cb`](https://github.com/anthropics/claude-agent-sdk-python/tree/347a1cb88d7652199399db1b7f914e0a459a6662)
+
+## Finding
+
+The Python Agent SDK does not implement Claude Max OAuth or the upstream
+Messages request protocol. It is an open-source controller for a separate
+Claude Code executable:
+
+```text
+Python API -> NDJSON/control protocol -> Claude Code executable -> OAuth/HTTP
+```
+
+Consequently, it is useful source material for implementing a C controller
+that launches Claude Code. It does not reveal the network behavior needed for
+a pure-C Claude subscription compatibility adapter. That behavior remains in
+the bundled, opaque executable.
+
+## Evidence
+
+The SDK's transport class is explicitly a Claude Code subprocess transport. It
+first locates a package-bundled `claude` executable, then searches for a system
+installation. It launches that executable with `--output-format stream-json`
+and `--input-format stream-json`, connects pipes to stdin/stdout, and parses
+one JSON object per output line. See
+[`subprocess_cli.py`](https://github.com/anthropics/claude-agent-sdk-python/blob/347a1cb88d7652199399db1b7f914e0a459a6662/src/claude_agent_sdk/_internal/transport/subprocess_cli.py#L111-L188),
+the
+[`stream-json` command construction](https://github.com/anthropics/claude-agent-sdk-python/blob/347a1cb88d7652199399db1b7f914e0a459a6662/src/claude_agent_sdk/_internal/transport/subprocess_cli.py#L282-L286),
+and its
+[`stdin`/`stdout` framing](https://github.com/anthropics/claude-agent-sdk-python/blob/347a1cb88d7652199399db1b7f914e0a459a6662/src/claude_agent_sdk/_internal/transport/subprocess_cli.py#L472-L502).
+
+The higher layer calls itself a bidirectional control protocol. It implements
+the initialization handshake, `control_request`/`control_response` routing,
+hook callbacks, tool-permission callbacks, SDK MCP callbacks, and normal
+message streaming. These are local SDK-to-CLI messages, not requests to
+Anthropic's service. See
+[`query.py`](https://github.com/anthropics/claude-agent-sdk-python/blob/347a1cb88d7652199399db1b7f914e0a459a6662/src/claude_agent_sdk/_internal/query.py#L65-L102),
+the
+[`initialize` request](https://github.com/anthropics/claude-agent-sdk-python/blob/347a1cb88d7652199399db1b7f914e0a459a6662/src/claude_agent_sdk/_internal/query.py#L172-L222),
+and the
+[`control_request` envelope](https://github.com/anthropics/claude-agent-sdk-python/blob/347a1cb88d7652199399db1b7f914e0a459a6662/src/claude_agent_sdk/_internal/query.py#L501-L528).
+
+The release build downloads Claude Code using Anthropic's installer and copies
+the resulting executable into the Python package. It does not build the
+network client from the Python sources. See
+[`download_cli.py`](https://github.com/anthropics/claude-agent-sdk-python/blob/347a1cb88d7652199399db1b7f914e0a459a6662/scripts/download_cli.py#L53-L85)
+and its
+[`copy_cli_to_bundle`](https://github.com/anthropics/claude-agent-sdk-python/blob/347a1cb88d7652199399db1b7f914e0a459a6662/scripts/download_cli.py#L112-L147).
+
+A source-wide search found no construction of `Authorization: Bearer`, no
+`api.anthropic.com` or `/v1/messages` request, and no OAuth token endpoint in
+the Python implementation. The only notable authentication handling is resume
+materialization: it knows the stored `claudeAiOauth` object and deliberately
+removes `refreshToken` before launching an isolated subprocess so that the
+subprocess cannot consume the Owner's single-use refresh token. It still
+delegates authentication and refresh behavior to Claude Code. See
+[`session_resume.py`](https://github.com/anthropics/claude-agent-sdk-python/blob/347a1cb88d7652199399db1b7f914e0a459a6662/src/claude_agent_sdk/_internal/session_resume.py#L320-L385).
+
+## What can be reimplemented in C
+
+A C implementation could reproduce the SDK-facing layer without Python:
+
+- spawn `claude` with the documented stream flags;
+- frame newline-delimited JSON safely across arbitrary pipe reads;
+- implement the initialize and correlated control request/response envelopes;
+- route permissions, hooks, and SDK MCP calls; and
+- consume the streamed assistant, tool, and result messages.
+
+That would remove Python, but it would not remove Claude Code or provide a
+native OpenBSD subscription client. It would also duplicate agent-loop
+features that fugu already owns.
+
+## Implication for fugu
+
+The Python source does not change the direct-C recommendation in
+[`claude-subscription-native-clients.md`](claude-subscription-native-clients.md).
+For a no-Node/no-Claude-Code design, the useful behavioral references remain
+projects that directly reproduce the current OAuth-compatible Messages
+request shape.
+
+If fugu instead copies the Python SDK's local protocol, the Claude Code child
+must run somewhere. Putting it inside `fugu-api` would contradict ADR-0001's
+steady-state rule that the credential custodian cannot exec or access the
+project. Preserving the existing security architecture would require the
+separate, tightly confined Subscription adapter already named in the domain
+model, with Claude Code tools disabled and no project access. The direct C
+compatibility path is smaller and keeps the Owner's token in `fugu-api`, but
+it remains unofficial and volatile.
blob - /dev/null
blob + bef21bec5847fe50643d305a34af406b5b06c5c4 (mode 644)
--- /dev/null
+++ docs/research/claude-subscription-native-clients.md
@@ -0,0 +1,164 @@
+# Native Claude subscription clients: evidence and implications for fugu
+
+Research date: 2026-07-16
+
+## Finding
+
+Third parties have successfully sent Claude Pro/Max subscription requests
+without invoking the Claude Agent SDK for inference. The best current evidence
+is [`griffinmartin/opencode-claude-auth`](https://github.com/griffinmartin/opencode-claude-auth),
+whose request path calls the Anthropic Messages endpoint directly with Claude
+Code OAuth credentials. Its committed live test from 2026-07-08 records 12 of
+14 models passing, and an independent 2026-07-10 issue log shows a request
+reaching the subscription quota check and receiving a normal rate-limit 429.
+That is strong evidence that direct OAuth inference was working in July 2026,
+not merely that an OAuth login screen existed.
+
+However, no maintained C implementation was found. The maintained direct
+clients are JavaScript/TypeScript projects. A native Rust attempt is archived
+after failing Anthropic's client validation, and a native Go implementation in
+Crush was removed at Anthropic's request. The protocol is small enough to
+implement in fugu's existing C HTTP stack, but it is private, changes often,
+and is explicitly outside Anthropic's documented third-party authentication
+path.
+
+## Classification
+
+| Project | Language/runtime | Inference path | Current evidence | Result for fugu |
+|---|---|---|---|---|
+| [`opencode-claude-auth`](https://github.com/griffinmartin/opencode-claude-auth) | TypeScript/Node inside OpenCode | Direct `api.anthropic.com` OAuth requests; no Agent SDK in the inference path | Active v2.0.0 release on 2026-07-08; [live smoke artifact](https://github.com/griffinmartin/opencode-claude-auth/blob/4ec411e/test-results/model-smoke-test.json); [2026-07-10 quota log](https://github.com/griffinmartin/opencode-claude-auth/issues/243) | Genuine current protocol reference, but not native and not officially supported |
+| [`auth2api`](https://github.com/AmazingAng/auth2api) | TypeScript/Node | Direct OAuth proxy with standalone login, refresh, request-shape compatibility, and account pools | Active project documenting Claude request fingerprint compatibility in its source/configuration | Corroborates the protocol, but still requires Node and has proxy-oriented scope |
+| [Crush](https://github.com/charmbracelet/crush/pull/1783) | Go | Previously had a native OAuth2 flow and Anthropic provider integration | Support was deliberately removed on 2026-01-07 following a request from Anthropic | Proof that a native implementation existed; also the clearest enforcement warning |
+| [`claude-code-mux`](https://github.com/9j/claude-code-mux) | Rust | Direct PKCE, token refresh, Bearer headers, and Messages API | Its own [issue #7](https://github.com/9j/claude-code-mux/issues/7) records Anthropic rejecting the credential as non-Claude-Code use; repository is archived | Useful negative example, not a working implementation base |
+| [`prototype-with-claude-max`](https://github.com/avivshaked/prototype-with-claude-max) | TypeScript/Node | Official Claude Agent SDK | Demonstrates one-person subscription use through the SDK | Good product-scope example, but does not remove Node or reimplement the protocol |
+| [`meridian`](https://github.com/rynfar/meridian) | TypeScript/Node | Official Agent SDK behind a local proxy | Feeds `CLAUDE_CODE_OAUTH_TOKEN` to the SDK | Wrapper, not a protocol reimplementation |
+| Community [Go](https://github.com/connerohnesorge/claude-agent-sdk-go) and [Rust](https://docs.rs/claude-cli-sdk/latest/claude_cli_sdk/fn.query.html) SDKs | Go/Rust library, Claude CLI subprocess | Spawn and control the installed `claude` executable | Their prerequisites/API documentation explicitly require or spawn the CLI | Native caller API only; still carries the Claude Code/Node dependency |
+
+## What the successful implementation actually does
+
+`opencode-claude-auth` is MIT licensed and self-contained. Its
+[`src/index.ts`](https://github.com/griffinmartin/opencode-claude-auth/blob/4ec411e/src/index.ts)
+installs a custom HTTP fetch path rather than calling the Agent SDK. It:
+
+- reads an access token produced by Claude Code and refreshes it directly at
+  Anthropic's OAuth token endpoint, falling back to the CLI only if refresh
+  fails;
+- sends `Authorization: Bearer` rather than `x-api-key`;
+- sends Claude Code OAuth beta, application, session, client, and user-agent
+  headers;
+- adds the required Claude Code identity system entry; and
+- sends the ordinary Anthropic Messages request and consumes ordinary SSE.
+
+Its [`src/transforms.ts`](https://github.com/griffinmartin/opencode-claude-auth/blob/4ec411e/src/transforms.ts)
+shows that authentication is not the whole compatibility surface. Anthropic
+has validated aspects of the request body as well. The plugin currently moves
+third-party system instructions into the first user message, preserves only
+the Claude identity and billing entries in `system`, maps tool names to the
+Claude Code convention, repairs tool-use/result pairs, and maps tool names
+back in the response. Its
+[`src/signing.ts`](https://github.com/griffinmartin/opencode-claude-auth/blob/4ec411e/src/signing.ts)
+reproduces the current billing-attribution value.
+
+The project's [changelog](https://github.com/griffinmartin/opencode-claude-auth/blob/4ec411e/CHANGELOG.md)
+is important evidence about maintenance cost. Between March and July 2026 it
+needed changes for new identifying headers, billing computation, system-prompt
+validation, tool-name casing, model-specific beta flags, token refresh, and
+new Claude Code versions. This is not a one-time implementation of a stable
+OAuth standard. It is continuing compatibility work against observed Claude
+Code behavior.
+
+The live smoke script is also informative. Its
+[`testModel`](https://github.com/griffinmartin/opencode-claude-auth/blob/4ec411e/scripts/test-models.ts)
+uses a minimal Claude Code identity entry plus the compatibility headers and
+successfully tests models directly. A full copied Claude Code system prompt is
+therefore not demonstrated as necessary for a basic request. fugu should not
+vendor a copied proprietary prompt; it should use the minimum functional
+identity/attribution fields and keep its own instructions as user-level
+context if this route is implemented.
+
+## Failed and removed native implementations
+
+`9j/claude-code-mux` is the closest discovered native implementation to what
+fugu would need. Its Rust source implements PKCE, refresh, Bearer auth, OAuth
+beta flags, and direct Messages requests. Nevertheless, its issue tracker
+records the upstream response: the credential was accepted as a credential
+but rejected because the request was not recognized as authorized Claude Code
+use. The project had no live OAuth integration test, stopped changing in
+November 2025, and is now archived. This demonstrates that copying the token
+exchange and headers alone is insufficient.
+
+Crush is stronger evidence that the implementation can be native. It is a Go
+coding agent and had a complete OAuth2 provider path. Its maintainers merged
+[`#1783`](https://github.com/charmbracelet/crush/pull/1783) to remove that path,
+stating that Anthropic requested the removal to align with its terms. Several
+forks restored it for personal use, but upstream did not. Similarly,
+OpenCode's earlier
+[`opencode-anthropic-auth`](https://github.com/anomalyco/opencode-anthropic-auth)
+implementation is now archived. These projects show technical feasibility but
+not a durable upstream-supported integration.
+
+## Official support boundary
+
+Anthropic documents `CLAUDE_CODE_OAUTH_TOKEN`, including the long-lived token
+from `claude setup-token`, for Claude Code and SDK automation in its
+[`authentication` documentation](https://code.claude.com/docs/en/authentication).
+The direct Claude API documents API keys and Workload Identity Federation,
+not Claude subscription tokens, as its supported authentication methods in
+the [Claude API authentication guide](https://platform.claude.com/docs/en/manage-claude/authentication).
+
+Most importantly, Anthropic's
+[`legal and compliance` documentation](https://code.claude.com/docs/en/legal-and-compliance)
+says subscription OAuth is intended for Claude Code and native Anthropic
+applications, directs developers of products/services to API-key or supported
+cloud authentication, and reserves enforcement rights. A one-Owner personal
+CLI is narrower than a hosted product and avoids routing another person's
+traffic, but a direct fugu client is still not an officially documented OAuth
+client. The repository and manual must not describe it as supported by
+Anthropic or guaranteed safe for the account.
+
+## Feasibility in fugu
+
+fugu already has most of the machinery a direct implementation needs:
+libtls HTTP, bounded JSON generation, Anthropic SSE parsing, tool-loop state,
+credential custody, retry policy, and secret-reflection filtering. A C
+compatibility adapter would therefore be much smaller than an Agent SDK port.
+It would need to:
+
+1. accept the Owner's long-lived `claude setup-token` token from the protected
+   configuration;
+2. select Bearer/OAuth compatibility headers and the Messages query form;
+3. rewrite the request's system/context placement and tool names for the
+   current Claude Code-compatible shape;
+4. reverse the tool-name mapping on the hostile SSE response;
+5. keep compatibility constants in one small, versioned module; and
+6. have a credentialed live regression that detects an upstream change before
+   release.
+
+No Node process, Agent SDK, new worker, filesystem widening, or `exec` promise
+is inherently needed. The OAuth token can remain inside the existing
+`fugu-api` credential custodian, so the original direct-provider security
+architecture can be preserved. The principal new risks are upstream
+compatibility, terms/enforcement, and the temptation to copy proprietary
+Claude Code material—not local privilege expansion.
+
+## Recommendation
+
+If both pure C and Max subscription use are non-negotiable, implement a small
+**experimental Claude subscription compatibility adapter**, not an Agent SDK
+reimplementation:
+
+- use `opencode-claude-auth` as the current behavioral reference;
+- reuse fugu's existing Anthropic transport and parser;
+- retain the inherent one-Owner release pattern and store the setup token
+  directly in fugu's protected configuration;
+- keep the direct API-key provider available as a separate supported route;
+- label subscription mode as unofficial and liable to stop working;
+- avoid browser OAuth, refresh-token storage, copied full prompts, account
+  pooling, proxies, and multi-user use; and
+- isolate volatile headers/body transformations behind one auditable module
+  with live tests.
+
+This achieves the requested runtime shape and preserves fugu's privilege
+separation. It does not eliminate the policy or maintenance risk. The evidence
+supports “technically feasible and currently demonstrated,” not “stable,
+permitted, or future-proof.”
blob - 504d7b09ea29a1e9cc6f6b8a0403d6e817a06d57
blob + a1b8b3c239479ded4bf7a9ed00014a5adfcdc419
--- etc/fugu.conf.sample
+++ etc/fugu.conf.sample
@@ -7,19 +7,33 @@
 # fugu refuses to start if the file (or any included or key file) is
 # world-accessible, group-writable, or owned by neither root nor the
 # invoking user.  Values with characters like '/' must be quoted.
+#
+# fugu is a personal tool for one Owner.  Claude subscription mode must not
+# be shared, hosted, exposed as an API, or used for another human's traffic.
 
 # Macros may be defined and reused with $-expansion.
 #anthropic_model = "claude-sonnet-4-6"
 
-# The model API key of the default provider (required to talk to a
-# provider).  Either inline or the first line of a separate file held
-# to the same secrecy standard.
+# Claude Pro/Max subscription mode.  Generate the one-year inference token
+# with `claude setup-token` and paste it directly into this protected file.
+# fugu is inherently a one-Owner personal program; there is no separate
+# identity or account-routing directive.  This experimental, unofficial
+# pure-C profile has a fixed Anthropic TLS endpoint and may stop working or
+# trigger upstream account enforcement.  Direct APIs remain the supported
+# route.
+#provider "claude"
+#model "claude-sonnet-4-6"
+#oauth_token "sk-ant-oat01-..."
+
+# A direct provider API key.  Use this instead of subscription OAuth for any
+# deployment that is not strictly personal.  Either inline or the first line
+# of a separate file held to the same secrecy standard.
 #api_key "sk-ant-..."
 #api_key_file "/etc/fugu.key"
 
-# Default provider: model id and wire protocol.
+# Default provider: model id and route.
 #model "claude-sonnet-4-6"
-#provider "anthropic"		# "anthropic" or "openai"
+#provider "anthropic"		# "claude", "anthropic", or "openai"
 
 # Endpoint overrides; the defaults follow the provider type
 # (api.anthropic.com or api.openai.com, port 443, /v1/messages or
@@ -43,7 +57,7 @@
 
 # Allow shell subprocesses to reach the network (the tools themselves
 # never can).  Enabling this permits a steered model to exfiltrate
-# readable files; that is the operator's trade.  Read-only subagent
+# readable files; that is the Owner's trade.  Read-only subagent
 # subprocesses remain networkless regardless.
 #allow_subprocess_net no
 
@@ -101,6 +115,11 @@
 #	api_path "/v1/chat/completions"
 #	api_key_file "/etc/fugu-work.key"
 #}
+#provider "personal" {
+#	type "claude"
+#	model "claude-opus-4-7"
+#	oauth_token "sk-ant-oat01-..."
+#}
 
 # When named providers exist, the agent tool's model palette qualifies
 # routes as "default:model" for the implicit provider and "name:model"
@@ -108,8 +127,10 @@
 # "@default:model".  With no named providers the model id is bare.  A
 # credentialless implicit provider is omitted from the palette.
 
-# Per-user and per-group overrides: the first matching block wins,
-# later blocks are not consulted.  No secrets in match blocks.
+# Legacy direct-API per-login policy: the first matching block wins and later
+# blocks are not consulted.  This is not a way to share subscription mode:
+# secrets remain forbidden in match blocks, and the release pattern is for one
+# Owner only.
 #match user "alice" {
 #	model "claude-opus-4-8"
 #	max_tokens 8192
blob - da415a4d239c12e9e04e999b611938d9ec82c9e0
blob + d1b228663ae914c56bda016364fe0548959da2a0
--- handoff/README.md
+++ handoff/README.md
@@ -70,6 +70,6 @@ bracketed paste, external-editor compose, a message qu
 mid-turn steering, edit diffs in the transcript, a turn-completion
 bell, the /context meter, prompt caching, transient-failure retry,
 the stale-edit freshness guard, read paging, multi_edit, background
-shell jobs, operator-protected paths, and headless print mode with
+shell jobs, Owner-protected paths, and headless print mode with
 JSON output. Tool hooks were considered and rejected: fugu is
 self-contained (charter.md non-goals).
blob - 203e367aa480c2571df241452f356f93ce0b2301
blob + 97cd41f060170cfc5dcf29672ebc19cc1040ae5e
--- handoff/behavior.md
+++ handoff/behavior.md
@@ -4,12 +4,13 @@ This is the functional contract, re-expressed from obs
 Everything is normative (MUST) unless marked *informative* or "free".
 Exact strings are pinned only where they are interface: command names,
 flag letters, config keywords, tool names and parameters, file paths.
-Prose the user or model reads (descriptions, error text, prompts) is
+Prose the Owner or model reads (descriptions, error text, prompts) is
 specified by required content, not by bytes — write it fresh, but it
 must convey everything listed.
 
-Conventions: "the lead" is the conversation the user drives; "a turn"
-is one user submission through to the assistant's completed response,
+Conventions: the **Owner** is fugu's one human user; "the lead" is the
+conversation the Owner drives; "a turn"
+is one Owner submission through to the assistant's completed response,
 including any tool calls in between.
 
 ## 1. Invocation
@@ -29,7 +30,7 @@ fugu [-cdlnv] [-o format] [-p prompt] [-r id]
 - **-n** — parse the configuration, resolve effective settings for the
   invoking user (defaults, global section, first matching `match`
   block), print them with every secret redacted, and exit. This is the
-  operator's dry-run; its exit status reflects config validity.
+  Owner's dry-run; its exit status reflects config validity.
 - **-d** — keep logging on stderr for the whole run instead of
   switching to syslog when the interface takes the terminal (§10).
 - **-v** — more verbose logging; repeatable (info, then debug).
@@ -105,7 +106,7 @@ Transcript behavior:
 - Errors are visually distinct from assistant output.
 - Scrollback: page keys always; while scrolled away from the tail, the
   status bar says so and how to return; new output does not yank the
-  view while the user is reading history unless they were at the tail.
+  view while the Owner is reading history unless they were at the tail.
 
 Status bar: an idle/busy indicator (distinct states for a normal
 turn, a summarization, and an automatic retry — the retry state shows
@@ -235,7 +236,7 @@ A line starting with `/` is a command, not a message:
 - **/quit**, **/exit** — end the session.
 
 Any other `/name` is looked up among loaded skills (§4); on a hit the
-skill fires, on a miss the user gets a one-line error. Completion
+skill fires, on a miss the Owner gets a one-line error. Completion
 covers built-ins and skills as in §2.1.
 
 A line starting with `#` is also not a message: the remainder is
@@ -252,7 +253,7 @@ file (§4) is never a `#` target.
 
 ## 4. Skills and project context
 
-Skills are user-installed prompt templates under `~/.fugu/skills/`,
+Skills are Owner-installed prompt templates under `~/.fugu/skills/`,
 loaded once at startup (creating or editing files takes effect next
 session).
 
@@ -267,8 +268,8 @@ The body below the closing fence is the template. Rule
   header, or with an empty body is silently skipped.
 - At most 256 skills load.
 
-Invocation (user): `/name [trailing text]` submits the skill body as
-the user's message, with any trailing text appended as a second
+Invocation (Owner): `/name [trailing text]` submits the skill body as
+the Owner's message, with any trailing text appended as a second
 paragraph. The turn then proceeds normally.
 
 Invocation (model): when at least one skill without
@@ -395,7 +396,7 @@ New in this revision, offered while `max_subagents` > 
       named by `subagent_model`/`subagent_provider` (§6), or to the
       lead's active model and provider when those are unset. The
       optional `model` parameter lets the lead route work by weight
-      per call — but its schema enumerates only the operator-configured
+      per call — but its schema enumerates only the Owner-configured
       palette: the default provider's `model`, each named provider
       block's `model`, and `subagent_model`. A value outside the enum
       is an error result, never a live request. When the palette spans
@@ -483,7 +484,7 @@ tool string argument that feeds a syscall or argv is r
 clear per-tool error (I13). Unknown tool names return an error result,
 not a crash.
 
-Cancellation: the user can abort an in-flight turn (interface-
+Cancellation: the Owner can abort an in-flight turn (interface-
 appropriate; reference used the drawer switch and quit paths). An
 aborted turn must satisfy §7's transactional rule; aborting also
 cancels the turn's running subagents and kills the background jobs
@@ -510,12 +511,19 @@ match group "students" { ... }
 
 Secrets may not appear inside `match` blocks.
 
+The Owner is inherent in fugu's personal invocation and release pattern, not a
+configuration value. There is no account-routing or Owner-identity directive.
+Subscription credentials MUST NOT be made available to a second human through
+`match` policy or any other mechanism. Subscription mode is personal CLI use
+only, never a hosted backend, public API, service, team facility, or
+customer-facing feature.
+
 Keywords (global or in `match` unless noted). Defaults in brackets.
 
 | keyword | meaning |
 |---|---|
 | `model` *string* | model id [claude-sonnet-4-6] |
-| `provider` *string* | wire protocol of the default provider: `anthropic` [default] or `openai` (the `/v1/chat/completions` dialect that third-party and local servers emulate, Bearer auth) |
+| `provider` *string* | default model route: experimental pure-C subscription compatibility `claude`, direct `anthropic` [default], or direct `openai` (the `/v1/chat/completions` dialect that third-party and local servers emulate, Bearer auth) |
 | `system` *string* | replaces the built-in system prompt entirely (single quoted line) |
 | `max_tokens` *number* | per-turn generation ceiling [4096] |
 | `context_limit` *number* | advisory token budget for the projection; when set, also the effective window for /context and the status gauge (§3) |
@@ -525,7 +533,7 @@ Keywords (global or in `match` unless noted). Defaults
 | `search_provider` *string* | search backend name [kagi] |
 | `max_subagents` *number* | concurrent subagent cap; 0 withholds the agent tool [4] |
 | `subagent_model` *string* | model id subagents use by default [unset: the lead's active model]; joins the agent tool's per-call palette (§5.12) |
-| `subagent_provider` *string* | name of a configured provider block (or the implicit default) whose protocol, endpoint, and key `subagent_model` is resolved against [the lead's active provider] |
+| `subagent_provider` *string* | name of a configured provider block (or the implicit default) whose route and credential `subagent_model` is resolved against [the lead's active provider] |
 | `max_bg_jobs` *number* | concurrent background shell jobs; 0 withholds shell_bg/shell_output/shell_kill [4] |
 | `project_context` yes\|no | read and inject FUGU.md/AGENTS.md project context (§4) [yes] |
 | `protect` *string* | whitespace-separated paths or glob(3) patterns relative to the working directory, expanded once at startup, that every tool-executing role unveils with empty permissions before touching untrusted input — contents kernel-unreachable (ENOENT-shaped error) by every tool, subprocess, and subagent, though names may remain visible in listings (invariants.md I5/I9); protecting a directory covers its subtree |
@@ -536,6 +544,7 @@ Keywords (global or in `match` unless noted). Defaults
 | `api_path` *string* | endpoint path [/v1/messages or /v1/chat/completions] |
 | `api_key` *string* | model API key (secret); `anthropic_key` is a legacy synonym for the anthropic provider, `api_key` wins if both |
 | `api_key_file` *string* | file whose first line is the key (secret; secrecy-checked); `anthropic_key_file` legacy synonym |
+| `oauth_token` *string* | Claude subscription inference token stored directly in the protected configuration; valid only for a `claude` route |
 | `kagi_token` / `kagi_token_file` *string* | search credential (secret) |
 | `http_allow` *string* | whitespace-separated `host[/pathprefix]` patterns enabling the http_request tool; `example.com` exact, `.example.com` includes subdomains, `*` any host (the manual must warn that `*` is an exfiltration channel); host match case-insensitive |
 | `http_block` *string* | same pattern form; a block always wins |
@@ -545,9 +554,10 @@ flat keys define the implicit default provider:
 
 ```
 provider "name" {
-	type		"anthropic" | "openai"
+	type		"claude" | "anthropic" | "openai"
 	model		...
-	api_key		...     # or api_key_file
+	oauth_token	...     # Claude
+	api_key		...     # direct APIs; or api_key_file
 	api_host	...
 	api_port	...
 	api_path	...
@@ -556,7 +566,9 @@ provider "name" {
 
 At startup the default provider (or the first block if no flat one) is
 active; `/model` switches model and provider together. Every block's
-key is a secret with the same handling as any other.
+credential is a secret with the same handling as any other. Endpoint
+directives on a `claude` route are rejected: its production host, TLS port,
+and Messages path are fixed so the Owner's Bearer token cannot be redirected.
 
 A sample file ships as `/etc/fugu.conf.sample`, fully commented.
 
@@ -595,28 +607,52 @@ its semantics are pinned:
   sent to the provider after a resume must be one the provider
   accepts (no orphaned tool calls, no adjacent same-role messages).
 - `/clear` and `/compact` never truncate the file; they append.
-- Full journals remain the user's plain files; the manual documents
+- Full journals remain the Owner's plain files; the manual documents
   that running fugu from `$HOME` lets the tools read them (CAVEATS).
 
 ## 8. Providers and streaming
 
-Two wire protocols: the Anthropic Messages API (default) and the
-OpenAI chat-completions dialect, both streaming (SSE), both supporting
-tool use, over TLS only via libtls. The conversation has one canonical
-in-memory shape; each protocol translates at the wire. Per-protocol
-model-listing calls back the picker. Usage (input/output tokens, plus
-cache-read/cache-write when reported) is surfaced to the UI when the
-provider reports it. Provider errors reach the user with the
-provider's own message and type where available, and with the real
-TLS failure reason on handshake problems. A turn that errors
-mid-stream obeys §7.
+There are three model routes. The direct Anthropic Messages API (default) and
+OpenAI chat-completions dialect stream SSE and support tool use over TLS via
+libtls. The `claude` route is experimental, unofficial, and personal-only. It
+uses the one Owner's long-lived inference token with one compile-time,
+versioned Claude Code-compatible request profile. No Node.js, Python, Claude
+Code executable, Agent SDK, browser login, token refresh, or credential-store
+discovery is part of fugu.
 
-Prompt caching: on the anthropic protocol every request — lead and
+The pure-C Subscription adapter translates one bounded canonical Projection
+at the Provider boundary. Its credential-free half supplies the minimum
+observed identity and billing attribution, selects model-specific betas,
+relocates fugu's instructions for the wire without mutating the Projection,
+and reversibly maps Tool names. Inside `fugu-api`, its credentialed half adds
+the Owner's Bearer token and fixed identifying headers before the existing
+libtls transport sends the request. Anthropic-shaped SSE is decoded under the
+same hostile-stream bounds as direct Messages. Provider Tool names are mapped
+back before IPC and still must match the exact Palette offered for that
+Generation; model-selected work occurs only in fugu's confined Tool roles.
+
+The compatibility profile is not an Anthropic-supported third-party API and
+may stop working or trigger account enforcement. Startup and failure messages
+name the active profile. fugu never probes alternate fingerprints, evades
+quota or safety controls, rotates accounts, or silently falls back to a
+billable API Provider or another subscription credential. Direct API routes
+are the supported alternative.
+
+The conversation has one canonical in-memory shape; each route translates at
+its boundary. Direct routes provide model-listing calls to the picker; a
+subscription route contributes its configured model and does not issue a
+listing call. Usage (input/output tokens, plus cache-read/cache-write when
+reported) is surfaced to the UI. Provider errors reach the Owner without
+revealing credentials. A turn that errors mid-stream obeys §7.
+
+Prompt caching: on the direct anthropic protocol every request — lead and
 subagent — marks cache breakpoints: `cache_control` ephemeral on the
 final tool definition, on the system prompt (emitted in block form),
 and on the newest conversation message that was present in the
 previous request of the session, moving that tail breakpoint forward
-each request. The openai dialect emits nothing (its caching is
+each request. The subscription profile preserves the Tool/tail breakpoints and
+cache-marks relocated fugu instructions, but never cache-marks its generated
+billing entry. The openai dialect emits nothing (its caching is
 server-side). Always on, no configuration keyword; a provider that
 rejects the field surfaces its error like any other. Cache state is
 in-memory per session and never journaled — `/clear`, `/compact`,
@@ -626,7 +662,7 @@ Steer delivery (§2.1) maps onto each dialect however 
 chooses, provided the journaled delivery position replays to a
 projection the provider accepts (§7).
 
-Transient failures: when a request fails before the first content
+For every HTTP Provider route, transient failures before the first content
 delta — connect or TLS handshake failure, HTTP 429, or any 5xx — the
 same request is retried automatically, up to 5 attempts total, with
 exponential backoff plus jitter (reference: 1, 2, 4, 8, 16 s, each ±
@@ -673,7 +709,7 @@ follow the same policy. Secrets never appear at any le
 ## 11. Documentation
 
 `fugu.1` and `fugu.conf.5`, mdoc(7), covering everything above that a
-user or operator can observe, including the honesty items of
+Owner can observe, including the honesty items of
 invariants.md §4 in CAVEATS, and a HISTORY note for the *pi* lineage
 (charter.md). The conversation drawer, skills, project context files,
 the agent and background-job tools, print mode, and the protect list
blob - e82a6bf6257e79211ca7c1d79bb28e6a3d62ac1c
blob + 7ea56caccd161a3b0dc5ecb78325a649434e3671
--- handoff/charter.md
+++ handoff/charter.md
@@ -6,9 +6,10 @@ This charter binds the clean-room rewrite. Everything 
 ## Identity
 
 - The program is **fugu**, an OpenBSD-native AI coding agent: an
-  interactive terminal program that pairs a large-language-model chat API
+  interactive terminal program for one human **Owner** that pairs a
+  large-language-model provider
   with local tools (file read/write/edit, shell, search, brokered web
-  access) so a model can work inside a project under the user's
+  access) so a model can work inside a project under the Owner's
   direction.
 - License: **ISC**, copyright Isaac <isaac@itm.works>. Every source file
   carries the standard ISC header.
@@ -29,6 +30,9 @@ This charter binds the clean-room rewrite. Everything 
   compat directory. A build on any other system failing is correct
   behavior. Portability abstractions would hollow out the security
   model, which is the point of the program.
+- Claude subscription mode is implemented in C through one versioned,
+  experimental compatibility profile. It requires no Node.js, Python, Claude
+  Code executable, Agent SDK, embedded interpreter, or ports runtime.
 - The program runs entirely unprivileged: no root, no setuid, no daemon,
   one invocation per session. Its only elevated bit is set-group-id
   `_fugu`, used solely to read the root-owned configuration
@@ -36,15 +40,22 @@ This charter binds the clean-room rewrite. Everything 
 
 ## Dependency charter
 
-- **Link against base libraries only.** No library from ports may appear
+- **Link the C binaries against base libraries only.** No library from ports may appear
   in any binary's `DT_NEEDED`, directly or by design. TLS is libtls
   (never raw libssl/libcrypto interfaces), terminal UI is base curses,
   IPC is `imsg(3)` from libutil.
 - Each binary links **only** the libraries its role requires (see
   invariants.md, I7). The expected shape is: libc everywhere; libutil
   where imsg is spoken; libtls only where TLS is spoken; libcurses only
-  in the terminal front end. A port `WANTLIB` of `c curses tls util` is
+  in the terminal front end. A port `WANTLIB` of
+  `c curses event tls util` is
   the ceiling.
+- **Experimental protocol boundary.** Personal Claude Pro/Max authentication
+  uses an observed Claude Code-compatible request profile, not an official
+  third-party API. Its volatile identity, attribution, beta, and Tool-name
+  transforms live behind one auditable C module and carry a visible profile
+  identifier. This is a compatibility and account-enforcement risk, never a
+  dependency exception or a claim of Anthropic support.
 - **Vendored code.** Where base offers nothing and a correct
   implementation is genuinely hard, a small, widely-used, single-file,
   permissively-licensed (MIT/ISC/BSD) source file MAY be vendored into
@@ -60,7 +71,8 @@ This charter binds the clean-room rewrite. Everything 
 - Domains where base offers **nothing** and small bespoke modules are
   expected: JSON emission/traversal (above the tokenizer), server-sent
   events (SSE) stream decoding, HTML-to-text reduction, the
-  Anthropic/OpenAI wire protocols, and web-search provider APIs.
+  direct Anthropic/OpenAI wire protocols, the narrow pure-C Subscription
+  adapter, and web-search provider APIs.
   Domains where base offers **everything** and bespoke code is wrong:
   config parsing, logging, IPC framing, TLS, memory allocation
   discipline, string/number conversion, atomic file I/O, directory
@@ -86,7 +98,9 @@ This charter binds the clean-room rewrite. Everything 
 ## Non-goals
 
 - Not a daemon, not a service, no listening sockets, no multi-session
-  server. One user, one terminal, one session per invocation.
+  server. One Owner, one terminal, one session per invocation. Claude
+  subscription mode is personal CLI use only: no hosting, public API, team
+  use, customer traffic, or delegation to another human.
 - No plugin ABI, no scripting language, no embedded interpreter.
 - No local model execution; fugu is a client of remote HTTPS APIs.
 - No Linux/macOS support (see Platform).
blob - fa2f14b53c08280e768550663d1c05d0ada2aadb
blob + 73e5c50c9a204d60a4dcd85f6e9591e1e66115b9
--- handoff/invariants.md
+++ handoff/invariants.md
@@ -17,9 +17,10 @@ required.
 
 ## 1. Assets
 
-- **A1** — model API credentials (Anthropic key, OpenAI-compatible keys).
+- **A1** — model credentials (the Owner's Claude subscription OAuth token,
+  Anthropic API key, and OpenAI-compatible API keys).
 - **A2** — the web-search credential (Kagi token).
-- **A3** — the user's files *outside* the project directory (including
+- **A3** — the Owner's files *outside* the project directory (including
   `~/.ssh`, other projects, and other fugu session journals).
 - **A4** — the project working tree's integrity (the thing fugu is
   allowed to change, but only via deliberate tool calls).
@@ -38,10 +39,9 @@ required.
 - **T2 — malicious web content.** Bodies fetched from the open web
   (HTML, headers, redirects) are hostile and may target memory-safety
   bugs in the parser that reduces them to text.
-- **T3 — a hostile or compromised API endpoint.** The TLS peer is
-  authenticated, but its stream (SSE frames, JSON) must be parsed as
-  untrusted data; a parser bug must not become code execution in a
-  privileged context.
+- **T3 — a hostile model backend.** Direct API and experimental subscription
+  SSE/JSON are untrusted data. A parser bug must not become model-selected
+  code execution or project-filesystem access.
 - **T4 — other local users** on a shared OpenBSD box.
 - **T5 — subprocesses run by the shell tool.** Anything `ksh -c` starts
   inherits T1's intent; confinement must survive `exec`.
@@ -59,12 +59,13 @@ them. MUST throughout.
 ### Credentials
 
 - **I1 — single-custodian credentials.** Each credential is held, after
-  startup, by exactly one role. The custodian of a model API credential
-  (A1) must be incapable of `execve(2)`, incapable of filesystem writes,
-  and incapable of opening files at all once its TLS trust store is
-  loaded; its network reach is TLS to the configured API endpoints and
-  nothing else. No credential is ever readable by the role that executes
-  tools, the role that renders the UI, or any subprocess.
+  startup, by exactly one role. The model-credential custodian, for direct API
+  and subscription routes alike, is incapable of `execve(2)`, incapable of
+  filesystem writes, and incapable of opening files at all once its TLS trust
+  store is loaded; its network reach is TLS to configured direct endpoints or
+  the subscription route's fixed Anthropic endpoint. No model credential is
+  ever readable by a model-selected tool role, subagent tool role, UI role,
+  journal, or shell subprocess.
 - **I2 — credentials never rest where the model can reach.** Secrets
   live in the root-owned, group-`_fugu`, mode-0640 `/etc/fugu.conf` (or
   files it points to, held to the same standard). The installed binary
@@ -91,12 +92,12 @@ them. MUST throughout.
 - **I4 — no network from tools, kernel-enforced.** The role that runs
   file and shell tools, and every subprocess it spawns, has no `inet`
   or `dns` capability. This is enforced with pledge execpromises (so it
-  survives `exec`), not by prompt or convention. The operator may opt
+  survives `exec`), not by prompt or convention. The Owner may opt
   in per-configuration (`allow_subprocess_net`), which widens only the
   subprocess ceiling, never the custodian roles.
 - **I5 — filesystem confinement, kernel-enforced.** Tool reads, writes,
   and subprocess filesystem access are confined by `unveil(2)` to: the
-  project directory (read/write/create) *minus* any operator-protected
+  project directory (read/write/create) *minus* any Owner-protected
   paths (behavior.md §6 `protect`), which are unveiled away before
   untrusted input is touched (a pure narrowing, per I9) so their
   contents are kernel-unreachable — the error is ENOENT-shaped, though
@@ -117,14 +118,14 @@ them. MUST throughout.
   (T2) is fetched and reduced to text in a role that holds no model
   credential, cannot `execve`, cannot write the filesystem, and cannot
   reach the tty. Full compromise of that role yields at most: the
-  search credential (A2) and outbound TLS. The API stream (T3) is
-  likewise parsed only in the credential custodian, which per I1 cannot
-  exec or write; a parser bug there must not reach the filesystem or a
-  shell.
+  search credential (A2) and outbound TLS. Direct API and subscription streams
+  (T3) are likewise parsed only in the credential custodian, which per I1
+  cannot exec, write, reopen files, or reach the project. A parser bug there
+  must not reach a model-selected shell or project file.
 - **I8 — SSRF guard.** Brokered fetches resolve names and then refuse
   private, loopback, and link-local destinations per resolved address
   (not per name), so DNS games cannot reach internal services. The
-  general HTTP tool exists only when the operator configures an
+  general HTTP tool exists only when the Owner configures an
   allowlist; deny-patterns win over allow-patterns; fugu attaches no
   credential of its own to brokered requests.
 
@@ -143,15 +144,16 @@ them. MUST throughout.
   IPC uses `imsg(3)`. Whatever the topology, the design must make each
   role's *authority* — who can ask whom for what — explicit and
   auditable in one place.
-- **I11 — least linkage.** Each executable links only the libraries its
-  role needs: libtls only where TLS is spoken, curses only where the
-  terminal is drawn, nothing from ports anywhere (charter.md). A role
-  parsing hostile input must not map code it never calls.
+- **I11 — least linkage.** Each executable links only the libraries its role
+  needs: libtls only where TLS is spoken and curses only where the terminal is
+  drawn. No ports library or language-runtime dependency may appear anywhere
+  in the program. A role parsing hostile input must not map code it never
+  calls.
 - **I12 — no ambient privilege.** No setuid. No daemon. No listening
   sockets. Per-invocation lifetime; children are torn down with the
   session by process group (a subprocess that calls `setsid(2)` can
   escape the group — §4 records this non-guarantee).
-  Session journals are the user's own files, mode 0600, under
+  Session journals are the Owner's own files, mode 0600, under
   `~/.fugu/sessions/`; the role that writes them is confined to that
   directory at the time it writes.
 
@@ -173,7 +175,7 @@ rewrite must make the bug classes unrepresentable, not
   session journal must be transactional at turn granularity: a turn
   that fails mid-flight (provider error, dropped stream, cancellation)
   leaves no partial records that a later resume would replay. Resuming
-  a session yields precisely the conversation state the user last saw
+  a session yields precisely the conversation state the Owner last saw
   after the most recent successful turn. (This is also a behavioral
   requirement; see behavior.md §7.)
 
@@ -198,11 +200,11 @@ rewrite must make the bug classes unrepresentable, not
 
 ## 4. Explicit non-guarantees
 
-Record these in the manual's CAVEATS so the operator's mental model is
+Record these in the manual's CAVEATS so the Owner's mental model is
 honest:
 
 - Within the project directory, the model can read, modify, or delete
-  anything not on the operator's `protect` list (that is A4 — the
+  anything not on the Owner's `protect` list (that is A4 — the
   granted authority). The protect set is a startup snapshot — files
   created later that would match are not covered — and protected
   names may still appear in directory listings even though their
@@ -215,7 +217,7 @@ honest:
   defenses above do not depend on what steers the model, but the
   manual must say this plainly.
 - With `allow_subprocess_net` enabled, exfiltration of readable files
-  by a steered model is possible; that is the operator's trade.
+  by a steered model is possible; that is the Owner's trade.
 - `http_allow "*"` converts the HTTP tool into an open POST channel;
   the manual must say so plainly.
 - Subprocess teardown is process-group-scoped: shell commands and
@@ -225,6 +227,13 @@ honest:
 - fugu does not defend against a hostile local root, a compromised
   kernel, or a malicious model *provider* beyond TLS authentication and
   parse-hardening.
+- A Claude subscription credential belongs to the one Owner inherent in the
+  personal installation and invocation pattern.
+  fugu is not an authentication boundary for sharing that subscription and
+  does not make hosted, public-API, service, team, or customer use acceptable.
+- Claude subscription compatibility is unofficial and undocumented. It may
+  stop working or cause Anthropic to limit or suspend the Owner's account;
+  fugu does not evade quotas, safety controls, or account enforcement.
 
 ## 5. Soft guidance (informative)
 
blob - 70d6c5a9a344a09b5dd84f2b0059ee965763ecd3
blob + 6dac0e8d6065a92ad1ecb31eb628cc8e20be4180
--- handoff/verification.md
+++ handoff/verification.md
@@ -18,7 +18,20 @@ rewrite.
   no framework, a `CHECK()` macro and a counter suffice.
 - **`make check`** performs a clean-room build (fresh objdir, no stale
   artifacts) followed by the full regress run; it is the gate before
-  any commit.
+  any commit. Its default transcript is concise: one live status line per
+  build phase and declared regression suite, per-suite check counts, and one
+  final suite/check/time summary. It stops at the first failure, prints that
+  phase or suite's complete captured output and focused rerun command, and
+  removes its temporary logs. Compiler warnings and every incomplete-proof
+  status (`SKIPPED`, `DISABLED`, `XFAIL`, or `XPASS`) fail the gate even when
+  the underlying regress target exits zero. An interruption prints only the
+  interrupted phase and rerun command and preserves the signal-derived exit
+  status.
+- **`make VERBOSE=1 check`** adds the complete live compiler and assertion
+  transcript while retaining the same phase framing, strict status policy,
+  and final summary. `VERBOSE` is otherwise unset; every nonempty value other
+  than exactly `1` is an error. Direct `make regress` remains the unwrapped
+  OpenBSD regression interface.
 - A hardened debug build mode (e.g. `make FUGU_DEBUG=1`) compiles with
   UBSan in trap mode, `-D_FORTIFY_SOURCE=2`, `-Wall -Wextra
   -Wformat=2`, and the regress suite must pass under it as well.
blob - 2374b39863eede171542726865d39b96985d6e07
blob + 50363a1783232aac08b522e59dc1afaa481e9df6
--- port/productivity/fugu/Makefile
+++ port/productivity/fugu/Makefile
@@ -6,8 +6,9 @@
 
 COMMENT =	privilege-separated AI coding agent
 
-V =		0.2
+V =		0.3
 DISTNAME =	fugu-${V}
+DISTFILES =	${DISTNAME}${EXTRACT_SUFX}
 CATEGORIES =	productivity
 
 HOMEPAGE =	https://itm.works/fugu/
@@ -17,7 +18,8 @@ MAINTAINER =	Isaac <isaac@itm.works>
 # ISC
 PERMIT_PACKAGE =	Yes
 
-# Uses pledge(2) and unveil(2); every dependency is in base.
+# Uses pledge(2) and unveil(2).  The experimental Claude subscription
+# compatibility profile is implemented in C and adds no runtime dependency.
 WANTLIB =	c curses event tls util
 
 SITES =		https://itm.works/pub/fugu/
blob - c9fa034eef90c2423ff5db5b2f98fa0a74797071
blob + f892ddfe628c14326ef5376ae37006455dc88c8a
--- port/productivity/fugu/pkg/DESCR
+++ port/productivity/fugu/pkg/DESCR
@@ -1,17 +1,23 @@
-fugu is an OpenBSD-native AI coding agent: a small, privilege-separated
-coding assistant written in C and sandboxed with pledge(2) and unveil(2).
-It pairs Anthropic Messages or an OpenAI-compatible endpoint with bounded
-local file and shell tools, brokered web search, fetch, and HTTP tools, and
-ephemeral read-only subagents.
+fugu is an OpenBSD-native personal AI coding agent for one Owner: the human
+who runs it and owns every credential it consumes. It is a small,
+privilege-separated coding assistant written in C and sandboxed with pledge(2)
+and unveil(2). It supports Anthropic Messages and OpenAI-compatible APIs, plus
+an experimental, unofficial pure-C compatibility route for the Owner's Claude
+Pro or Max subscription. The subscription route requires no Node.js, Python,
+Claude Code executable, or Agent SDK runtime, but its undocumented protocol
+may stop working or trigger upstream account enforcement.
 
 Six executables have disjoint privileges.  The terminal role has no network
 or filesystem access; the API role holds the model credential but cannot
 open files, write, or execute programs; the web role cannot write or execute;
 and model-selected local tools cannot reach outside the project tree or use
-the network unless the operator explicitly permits subprocess networking.
+the network unless the Owner explicitly permits subprocess networking.
 Subagent local tools and subprocesses remain kernel-confined read-only and
 networkless regardless of that permission; any web access stays brokered.
-fugu is neither a daemon nor a service.
+Subscription mode is for the Owner alone. fugu must not be hosted, exposed as
+an API, shared with another human, or used to route one person's Pro/Max quota
+to team traffic. Direct API providers are the supported route for services,
+teams, and customer traffic. fugu is neither a daemon nor a service.
 
 fugu is a clean-room reimplementation, in behavior rather than source, of
 the pi agent by Mario Zechner.
blob - fc82d725238f321e7772e907d668f823f1a322ea
blob + e07e1ac9c829c800190315fad645eabdf26200f9
--- regress/Makefile
+++ regress/Makefile
@@ -1,4 +1,4 @@
-SUBDIR=	agentcfg anthropic buf conf deploy diff editor generation http imsgev journal json log markdown \
+SUBDIR=	agentcfg anthropic buf claude_subscription conf deploy diff editor generation http imsgev journal json local_port log markdown \
 	model_window openai output print sandbox skills sse term tools turn \
 	turn_mechanics turn_txn ui_config web xmalloc
 
blob - a8022ac95b25226875c4ad607b6180a345ec96b0
blob + 0c773c935909293f3137f33b22513da99a2e8bb1
--- regress/README
+++ regress/README
@@ -162,7 +162,10 @@ session journal (behavior.md 7)		journal, turn
 session listing (behavior.md 1)		journal, turn (plain, JSON, NDJSON)
 machine output (behavior.md 1)		output (schema/hostile bytes); print
 logging (behavior.md 10)		log, sandbox, turn (mesh propagation)
-deployment (behavior.md 9/11)		deploy (layout, manuals, linkage, port)
+deployment (behavior.md 9/11)		deploy (layout, manuals, linkage, port);
+					local_port (clean-commit archive,
+					computed port identity, checksums,
+					atomic staging and rollback)
 
 Module					Suite(s)
 ------					--------
@@ -205,3 +208,4 @@ src/fugu/skills.c			skills (load/parse); turn (dispatc
 src/fugu/output.c			output, print
 src/fugu-web (fetch.c, main.c)		web (hermetic TLS flows); sandbox
 					(steady confinement and handshake)
+scripts/local-port			local_port
blob - 613d2972a0135cf324f427183c4c432c5fd82857
blob + e9067ff4fafbb22cbc7d3f953d6bddb9c041a41f
--- regress/conf/conf_test.c
+++ regress/conf/conf_test.c
@@ -495,6 +495,55 @@ test_providers(void)
 }
 
 static void
+test_claude_subscription(void)
+{
+	struct fugu_conf	*c;
+	struct fugu_provider	*p;
+
+	CHECK(parse_ok(
+	    "provider \"claude\"\n"
+	    "oauth_token \"sk-ant-oat01-owner\"\n",
+	    &alice, &c));
+	CHECK(c->provider_type == PROVIDER_CLAUDE);
+	CHECK(strcmp(conf_effective_key(c), "sk-ant-oat01-owner") == 0);
+	conf_free(c);
+
+	CHECK(parse_fails("owner \"alice\"\n"));
+	CHECK(parse_fails(
+	    "provider \"claude\"\n"
+	    "api_key \"not-an-oauth-token\"\n"));
+	CHECK(parse_fails("oauth_token \"sk-ant-oat01-owner\"\n"));
+	CHECK(parse_fails(
+	    "provider \"claude\"\n"
+	    "oauth_token \"sk-ant-oat01-owner\"\n"
+	    "api_host \"example.com\"\n"));
+	CHECK(parse_fails(
+	    "provider \"claude\"\n"
+	    "oauth_token \"sk-ant-oat01-owner\"\n"
+	    "claude_node \"/usr/local/bin/node\"\n"));
+	CHECK(parse_fails(
+	    "provider \"claude\"\n"
+	    "oauth_token_file \"/etc/fugu-claude-oauth.token\"\n"));
+	CHECK(parse_fails(
+	    "match user \"alice\" { oauth_token \"secret\" }\n"));
+
+	CHECK(parse_ok(
+	    "provider \"personal\" {\n"
+	    "\ttype \"claude\"\n"
+	    "\tmodel \"claude-opus-4-7\"\n"
+	    "\toauth_token \"sk-ant-oat01-owner\"\n"
+	    "}\n", &alice, &c));
+	p = TAILQ_FIRST(&c->providers);
+	CHECK(p != NULL && p->type == PROVIDER_CLAUDE);
+	CHECK(p != NULL && p->oauth_token != NULL);
+	CHECK(c->initial_provider_slot == 1);
+	CHECK(c->provider_type == PROVIDER_CLAUDE);
+	conf_wipe_secrets(c);
+	CHECK(p->oauth_token == NULL);
+	conf_free(c);
+}
+
+static void
 test_macros(void)
 {
 	struct fugu_conf	*c;
@@ -673,6 +722,21 @@ test_dump_redacts(void)
 	CHECK(strstr(buf, "kagi_token <redacted>") != NULL);
 	free(buf);
 	conf_free(c);
+
+	buf = NULL;
+	sz = 0;
+	CHECK(parse_ok(
+	    "provider \"claude\"\n"
+	    "oauth_token \"sk-ant-oat01-dump\"\n", &alice, &c));
+	if ((f = open_memstream(&buf, &sz)) == NULL)
+		err(1, "open_memstream");
+	conf_dump(c, f);
+	fclose(f);
+	CHECK(strstr(buf, "sk-ant-oat01-dump") == NULL);
+	CHECK(strstr(buf, "oauth_token <redacted>") != NULL);
+	CHECK(strstr(buf, "owner \"") == NULL);
+	free(buf);
+	conf_free(c);
 }
 
 static void
@@ -746,6 +810,7 @@ main(void)
 	test_secrecy();
 	test_keyfiles();
 	test_providers();
+	test_claude_subscription();
 	test_macros();
 	test_numbers();
 	test_model_bounds();
blob - /dev/null
blob + d41a574d7221f6b5c98ebd36d674219f9bae3880 (mode 644)
--- /dev/null
+++ regress/claude_subscription/Makefile
@@ -0,0 +1,5 @@
+PROG=	claude_subscription_test
+SRCS=	claude_subscription_test.c claude_sub_req.c claude_sub_profile.c \
+	claude_sub_wire.c msg.c json.c buf.c log.c xmalloc.c
+
+.include <bsd.regress.mk>
blob - /dev/null
blob + 20a70df34b29304e4333199bbc867a361720c58c (mode 644)
--- /dev/null
+++ regress/claude_subscription/claude_subscription_test.c
@@ -0,0 +1,343 @@
+/*
+ * Copyright (c) 2026 Isaac <isaac@itm.works>
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include <sys/queue.h>
+
+#include <stdint.h>
+#include <stdlib.h>
+#include <string.h>
+#include <syslog.h>
+
+#include "log.h"
+#include "buf.h"
+#include "msg.h"
+#include "json.h"
+#include "claude_sub.h"
+
+#include "regress.h"
+
+static int
+has(const struct buf *b, const char *needle)
+{
+	return (memmem(b->data, b->len, needle, strlen(needle)) != NULL);
+}
+
+static int
+count_cache(const struct buf *b)
+{
+	static const char needle[] = "\"cache_control\"";
+	size_t off, nlen = sizeof(needle) - 1;
+	void *p;
+	int count = 0;
+
+	for (off = 0; off + nlen <= b->len; off++) {
+		p = memmem(b->data + off, b->len - off, needle, nlen);
+		if (p == NULL)
+			break;
+		count++;
+		off = (size_t)((u_char *)p - b->data);
+	}
+	return (count);
+}
+
+static char *
+json_string(const struct json *j, int obj, const char *key, size_t *len)
+{
+	return (json_get_str(j, json_obj_get(j, obj, key), len));
+}
+
+static void
+test_request_identity(void)
+{
+	static const char billing[] = "x-anthropic-billing-header: "
+	    "cc_version=2.1.185.ecf; cc_entrypoint=sdk-cli; cch=fa690;";
+	static const char identity[] =
+	    "You are Claude Code, Anthropic's official CLI for Claude.";
+	struct claude_sub_req req;
+	struct msglist conv;
+	struct msg *m;
+	struct block *original;
+	struct buf out;
+	struct json j;
+	char *s;
+	size_t len;
+	int system, messages, message, content, block;
+
+	TAILQ_INIT(&conv);
+	m = msg_new(ROLE_USER);
+	original = msg_add_text(m, "hey", 3);
+	TAILQ_INSERT_TAIL(&conv, m, entry);
+
+	memset(&req, 0, sizeof(req));
+	req.model = "claude-sonnet-4-6";
+	req.max_tokens = 4096;
+	req.system = "be terse";
+	req.systemlen = 8;
+	req.conv = &conv;
+	req.cache = 1;
+	req.cache_tail_msg = -1;
+
+	buf_init(&out);
+	CHECK(claude_sub_build_request(&out, &req) == 0);
+	CHECK(json_parse(&j, out.data, out.len, 0) == 0);
+
+	system = json_obj_get(&j, 0, "system");
+	CHECK(json_is_array(&j, system));
+	CHECK(json_arr_len(&j, system) == 2);
+	block = json_arr_get(&j, system, 0);
+	s = json_string(&j, block, "text", &len);
+	CHECK(s != NULL && len == sizeof(billing) - 1 &&
+	    memcmp(s, billing, len) == 0);
+	free(s);
+	CHECK(json_obj_get(&j, block, "cache_control") == -1);
+	block = json_arr_get(&j, system, 1);
+	s = json_string(&j, block, "text", &len);
+	CHECK(s != NULL && len == sizeof(identity) - 1 &&
+	    memcmp(s, identity, len) == 0);
+	free(s);
+
+	messages = json_obj_get(&j, 0, "messages");
+	CHECK(json_arr_len(&j, messages) == 1);
+	message = json_arr_get(&j, messages, 0);
+	content = json_obj_get(&j, message, "content");
+	CHECK(json_arr_len(&j, content) == 2);
+	block = json_arr_get(&j, content, 0);
+	s = json_string(&j, block, "text", &len);
+	CHECK(s != NULL && len == 8 && memcmp(s, "be terse", 8) == 0);
+	free(s);
+	CHECK(json_obj_get(&j, block, "cache_control") != -1);
+	block = json_arr_get(&j, content, 1);
+	s = json_string(&j, block, "text", &len);
+	CHECK(s != NULL && len == 3 && memcmp(s, "hey", 3) == 0);
+	free(s);
+
+	/* Wire relocation must not mutate the canonical Projection. */
+	CHECK(TAILQ_FIRST(&m->blocks) == original);
+	CHECK(TAILQ_LAST(&m->blocks, blocklist) == original);
+	CHECK(original->textlen == 3 && memcmp(original->text, "hey", 3) == 0);
+
+	json_done(&j);
+	buf_free(&out);
+	msglist_free(&conv);
+}
+
+static void
+test_tool_mapping(void)
+{
+	static const struct {
+		const char *canonical;
+		const char *wire;
+	} builtins[] = {
+		{ "read", "mcp_Read" },
+		{ "write", "mcp_Write" },
+		{ "edit", "mcp_Edit" },
+		{ "multi_edit", "mcp_Multi_edit" },
+		{ "shell", "mcp_Shell" },
+		{ "grep", "mcp_Grep" },
+		{ "find", "mcp_Find" },
+		{ "ls", "mcp_Ls" },
+		{ "web_search", "mcp_Web_search" },
+		{ "web_fetch", "mcp_Web_fetch" },
+		{ "http_request", "mcp_Http_request" },
+		{ "shell_bg", "mcp_Shell_bg" },
+		{ "shell_output", "mcp_Shell_output" },
+		{ "shell_kill", "mcp_Shell_kill" },
+		{ "skill", "mcp_Skill" },
+		{ "agent", "mcp_Agent" },
+	};
+	char name[64];
+	size_t i;
+
+	for (i = 0; i < sizeof(builtins) / sizeof(builtins[0]); i++) {
+		CHECK(claude_sub_tool_map(name, sizeof(name),
+		    builtins[i].canonical) == 0 &&
+		    strcmp(name, builtins[i].wire) == 0);
+		CHECK(claude_sub_tool_unmap(name, sizeof(name),
+		    builtins[i].wire) == 0 &&
+		    strcmp(name, builtins[i].canonical) == 0);
+	}
+	CHECK(claude_sub_tool_map(name, sizeof(name), "Read") == -1);
+	CHECK(claude_sub_tool_map(name, sizeof(name), "bad-name") == -1);
+	CHECK(claude_sub_tool_unmap(name, sizeof(name), "read") == -1);
+	CHECK(claude_sub_tool_unmap(name, sizeof(name), "mcp_read") == -1);
+	CHECK(claude_sub_tool_map(name, 8, "read") == -1);
+}
+
+static void
+test_duplicate_tools(void)
+{
+	struct claude_sub_req req;
+	struct msglist conv;
+	struct msg *m;
+	struct tool_def tools[2];
+	struct buf out;
+
+	TAILQ_INIT(&conv);
+	m = msg_new(ROLE_USER);
+	msg_add_text(m, "hello", 5);
+	TAILQ_INSERT_TAIL(&conv, m, entry);
+	tools[0].name = "read";
+	tools[0].description = "first";
+	tools[0].schema = "{\"type\":\"object\"}";
+	tools[1].name = "read";
+	tools[1].description = "duplicate";
+	tools[1].schema = "{\"type\":\"object\"}";
+
+	memset(&req, 0, sizeof(req));
+	req.model = "claude-sonnet-4-6";
+	req.max_tokens = 4096;
+	req.tools = tools;
+	req.ntools = 2;
+	req.conv = &conv;
+	buf_init(&out);
+	CHECK(claude_sub_build_request(&out, &req) == -1);
+	CHECK(out.len == 0);
+
+	buf_free(&out);
+	msglist_free(&conv);
+}
+
+static void
+test_request_tools(void)
+{
+	static const char billing[] = "x-anthropic-billing-header: "
+	    "cc_version=2.1.185.4c2; cc_entrypoint=sdk-cli; cch=35360;";
+	struct claude_sub_req req;
+	struct msglist conv;
+	struct msg *m;
+	struct tool_def tools[2];
+	struct buf out;
+
+	TAILQ_INIT(&conv);
+	m = msg_new(ROLE_USER);
+	msg_add_text(m, "read the file", 13);
+	TAILQ_INSERT_TAIL(&conv, m, entry);
+	m = msg_new(ROLE_ASSISTANT);
+	msg_add_text(m, "ok", 2);
+	msg_add_tool_use(m, "tu_1", "read", "{\"path\":\"a.c\"}", 14);
+	TAILQ_INSERT_TAIL(&conv, m, entry);
+	m = msg_new(ROLE_USER);
+	msg_add_tool_result(m, "tu_1", "body", 4, 0);
+	TAILQ_INSERT_TAIL(&conv, m, entry);
+	tools[0].name = "read";
+	tools[0].description = "read a file";
+	tools[0].schema = "{\"type\":\"object\"}";
+	tools[1].name = "write";
+	tools[1].description = "write a file";
+	tools[1].schema = "{\"type\":\"object\"}";
+
+	memset(&req, 0, sizeof(req));
+	req.model = "claude-sonnet-4-6";
+	req.max_tokens = 8192;
+	req.system = "system";
+	req.systemlen = 6;
+	req.tools = tools;
+	req.ntools = 2;
+	req.conv = &conv;
+	req.cache = 1;
+	req.cache_tail_msg = 1;
+
+	buf_init(&out);
+	CHECK(claude_sub_build_request(&out, &req) == 0);
+	CHECK(has(&out, billing));
+	CHECK(has(&out, "\"name\":\"mcp_Read\""));
+	CHECK(has(&out, "\"name\":\"mcp_Write\""));
+	CHECK(!has(&out, "\"name\":\"read\""));
+	CHECK(has(&out, "\"tool_use_id\":\"tu_1\""));
+	CHECK(count_cache(&out) == 3);
+
+	buf_free(&out);
+	msglist_free(&conv);
+}
+
+static void
+test_headers(void)
+{
+	char headers[8192];
+
+	CHECK(claude_sub_build_headers(headers, sizeof(headers),
+	    "sk-ant-oat01-fixture", "claude-sonnet-4-6",
+	    "11111111-1111-4111-8111-111111111111",
+	    "22222222-2222-4222-8222-222222222222") == 0);
+	CHECK(strstr(headers,
+	    "Authorization: Bearer sk-ant-oat01-fixture\r\n") != NULL);
+	CHECK(strstr(headers, "x-api-key:") == NULL);
+	CHECK(strstr(headers, "anthropic-version: 2023-06-01\r\n") != NULL);
+	CHECK(strstr(headers, "oauth-2025-04-20") != NULL);
+	CHECK(strstr(headers, "claude-code-20250219") != NULL);
+	CHECK(strstr(headers, "x-app: cli\r\n") != NULL);
+	CHECK(strstr(headers, "user-agent: claude-cli/2.1.185 "
+	    "(external, sdk-cli)\r\n") != NULL);
+	CHECK(strstr(headers, "x-claude-code-session-id: "
+	    "11111111-1111-4111-8111-111111111111\r\n") != NULL);
+	CHECK(strstr(headers, "x-client-request-id: "
+	    "22222222-2222-4222-8222-222222222222\r\n") != NULL);
+	CHECK(strstr(headers, "content-type: application/json\r\n") != NULL);
+
+	CHECK(claude_sub_build_headers(headers, sizeof(headers),
+	    "token", "claude-haiku-4-5",
+	    "11111111-1111-4111-8111-111111111111",
+	    "22222222-2222-4222-8222-222222222222") == 0);
+	CHECK(strstr(headers, "oauth-2025-04-20") != NULL);
+	CHECK(strstr(headers, "interleaved-thinking-2025-05-14") == NULL);
+	CHECK(claude_sub_build_headers(headers, 64, "token", "model",
+	    "session", "request") == -1);
+	CHECK(claude_sub_build_headers(headers, sizeof(headers),
+	    "bad\r\ntoken", "model", "session", "request") == -1);
+}
+
+static void
+test_unicode_billing(void)
+{
+	static const char prompt[] = "abcd" "\xf0\x9f\x98\x80"
+	    "efghijklmnopqrstuv";
+	static const char billing[] = "x-anthropic-billing-header: "
+	    "cc_version=2.1.185.326; cc_entrypoint=sdk-cli; cch=7f3b5;";
+	struct claude_sub_req req;
+	struct msglist conv;
+	struct msg *m;
+	struct buf out;
+
+	TAILQ_INIT(&conv);
+	m = msg_new(ROLE_USER);
+	msg_add_text(m, prompt, sizeof(prompt) - 1);
+	TAILQ_INSERT_TAIL(&conv, m, entry);
+	memset(&req, 0, sizeof(req));
+	req.model = "claude-sonnet-4-6";
+	req.max_tokens = 32;
+	req.conv = &conv;
+	req.cache_tail_msg = -1;
+	buf_init(&out);
+	CHECK(claude_sub_build_request(&out, &req) == 0);
+	CHECK(has(&out, billing));
+	buf_free(&out);
+	msglist_free(&conv);
+}
+
+int
+main(void)
+{
+	log_init(LOG_TO_STDERR, 0, LOG_USER);
+
+	test_request_identity();
+	test_tool_mapping();
+	test_duplicate_tools();
+	test_request_tools();
+	test_headers();
+	test_unicode_billing();
+
+	REGRESS_END();
+}
blob - /dev/null
blob + ea245c72e13dfa79637a690cd18a584f433e8f1c (mode 644)
--- /dev/null
+++ regress/local_port/Makefile
@@ -0,0 +1,6 @@
+REGRESS_TARGETS=	run-local-port
+
+run-local-port:
+	sh ${.CURDIR}/run.sh ${.CURDIR}/../..
+
+.include <bsd.regress.mk>
blob - /dev/null
blob + e4fa9c1c953f75b146c202c66daef665238b5ee5 (mode 755)
--- /dev/null
+++ regress/local_port/run.sh
@@ -0,0 +1,202 @@
+#!/bin/sh
+#
+# Copyright (c) 2026 Isaac <isaac@itm.works>
+#
+# Permission to use, copy, modify, and distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+set -u
+
+root=$1
+checks=0
+fails=0
+
+pass()
+{
+	checks=$((checks + 1))
+	printf 'ok %d - %s\n' "$checks" "$1"
+}
+
+fail()
+{
+	checks=$((checks + 1))
+	fails=$((fails + 1))
+	printf 'FAIL %d - %s\n' "$checks" "$1"
+}
+
+ok()
+{
+	if [ "$2" -eq 0 ]; then
+		pass "$1"
+	else
+		fail "$1"
+	fi
+}
+
+dir=$(mktemp -d /tmp/fugu-local-port-regress.XXXXXXXXXX)
+trap 'rm -rf "$dir"' EXIT HUP INT TERM
+repo=$dir/repo
+bindir=$dir/bin
+portsdir=$dir/ports
+portdir=$portsdir/mystuff/productivity/fugu
+distdir=$portsdir/distfiles
+
+mkdir -p "$repo/scripts" "$repo/port/productivity" "$bindir" \
+    "$portsdir/mystuff/productivity" "$distdir" \
+    "$portsdir/pobj" "$portsdir/packages" "$portsdir/plist"
+cp "$root/Makefile" "$repo/Makefile"
+cp "$root/scripts/local-port" "$repo/scripts/local-port"
+cp -R "$root/port/productivity/fugu" "$repo/port/productivity/fugu"
+printf 'committed payload\n' >"$repo/payload.txt"
+
+cat >"$bindir/port-make" <<'EOF'
+#!/bin/sh
+set -eu
+
+printf '%s\n' "$*" >>"$FUGU_TEST_MAKE_LOG"
+request=
+target=
+for arg in "$@"; do
+	case "$arg" in
+	show=*)	request=${arg#show=} ;;
+	makesum|checksum)	target=$arg ;;
+	esac
+done
+
+if [ -n "$request" ]; then
+	case "$request" in
+	FULLPKGPATH)	printf '%s\n' "$FUGU_TEST_FULLPKGPATH" ;;
+	FULLPKGNAME)	printf '%s\n' fugu-0.3 ;;
+	DISTNAME)	printf '%s\n' fugu-0.3 ;;
+	DISTFILES)	printf '%s\n' fugu-0.3.tar.gz ;;
+	EXTRACT_SUFX)	printf '%s\n' .tar.gz ;;
+	FULLDISTDIR)	printf '%s\n' "$FUGU_TEST_DISTDIR" ;;
+	CHECKSUM_FILE)	printf '%s/distinfo\n' "$FUGU_TEST_PORTDIR" ;;
+	WRKDIR)	printf '%s/pobj/fugu-0.3\n' "$FUGU_TEST_PORTSDIR" ;;
+	PACKAGE_REPOSITORY)	printf '%s/packages\n' "$FUGU_TEST_PORTSDIR" ;;
+	PLIST_REPOSITORY)	printf '%s/plist\n' "$FUGU_TEST_PORTSDIR" ;;
+	*)	exit 1 ;;
+	esac
+	exit 0
+fi
+
+case "$target" in
+makesum)
+	cksum "$FUGU_TEST_DISTDIR/fugu-0.3.tar.gz" \
+	    >"$FUGU_TEST_PORTDIR/distinfo"
+	;;
+checksum)
+	test -s "$FUGU_TEST_PORTDIR/distinfo" &&
+	    test -s "$FUGU_TEST_DISTDIR/fugu-0.3.tar.gz"
+	;;
+*)	exit 1 ;;
+esac
+EOF
+
+cat >"$bindir/doas" <<'EOF'
+#!/bin/sh
+set -eu
+printf '%s\n' "$*" >>"$FUGU_TEST_DOAS_LOG"
+exec "$@"
+EOF
+chmod 0755 "$repo/scripts/local-port" "$bindir/port-make" "$bindir/doas"
+
+git -C "$repo" init -q
+git -C "$repo" add .
+env GIT_AUTHOR_NAME=fugu GIT_AUTHOR_EMAIL=fugu@example.invalid \
+    GIT_COMMITTER_NAME=fugu GIT_COMMITTER_EMAIL=fugu@example.invalid \
+    git -C "$repo" commit -q -m local-port-regress
+commit=$(git -C "$repo" rev-parse HEAD)
+
+export FUGU_LOCAL_PORTDIR="$portdir"
+export FUGU_LOCAL_PORT_MAKE="$bindir/port-make"
+export FUGU_LOCAL_PORT_DOAS="$bindir/doas"
+export FUGU_TEST_PORTDIR="$portdir"
+export FUGU_TEST_PORTSDIR="$portsdir"
+export FUGU_TEST_DISTDIR="$distdir"
+export FUGU_TEST_FULLPKGPATH=productivity/fugu
+export FUGU_TEST_MAKE_LOG="$dir/make.log"
+export FUGU_TEST_DOAS_LOG="$dir/doas.log"
+
+make -s -C "$repo" local-port >"$dir/first.out" 2>"$dir/first.err"
+rc=$?
+ok "target: make local-port succeeds for a clean commit" "$rc"
+
+test -s "$distdir/fugu-0.3.tar.gz" && test -s "$portdir/distinfo"
+ok "artifacts: distfile and generated distinfo are retained" $?
+
+tar -tzf "$distdir/fugu-0.3.tar.gz" >"$dir/archive.list"
+awk '$0 != "fugu-0.3" && $0 !~ /^fugu-0\.3\// { bad = 1 }
+    END { exit bad }' \
+    "$dir/archive.list" &&
+    grep -qx 'fugu-0.3/payload.txt' "$dir/archive.list" &&
+    ! grep -q '/\.git/' "$dir/archive.list"
+ok "archive: exact commit has one DISTNAME top-level directory" $?
+
+grep -Fq "commit:            $commit" "$dir/first.out" &&
+    grep -Fq "port directory:     $portdir" "$dir/first.out" &&
+    grep -Fq "make -C $portdir package" "$dir/first.out"
+ok "output: provenance, staged path, and next package command are reported" $?
+
+grep -q 'show=FULLPKGPATH' "$dir/make.log" &&
+    grep -q '^.*makesum$' "$dir/make.log" &&
+    grep -q '^.*checksum$' "$dir/make.log"
+ok "framework: computed package path, makesum, and checksum are used" $?
+
+grep -Fq "install -m 0644" "$dir/doas.log" &&
+    grep -Fq "$distdir/fugu-0.3.tar.gz" "$dir/doas.log"
+ok "privilege: only staging operations cross the doas adapter" $?
+
+printf 'uncommitted\n' >"$repo/dirty.txt"
+if make -s -C "$repo" local-port >"$dir/dirty.out" \
+    2>"$dir/dirty.err"; then
+	rc=1
+else
+	rc=0
+fi
+grep -q 'working tree is not clean' "$dir/dirty.err" || rc=1
+test -s "$portdir/distinfo" || rc=1
+ok "safety: a dirty tree is rejected before replacing the staged port" "$rc"
+rm "$repo/dirty.txt"
+
+printf 'preserve staged port\n' >"$portdir/sentinel"
+export FUGU_TEST_FULLPKGPATH=productivity/not-fugu
+if make -s -C "$repo" local-port >"$dir/path.out" \
+    2>"$dir/path.err"; then
+	rc=1
+else
+	rc=0
+fi
+grep -q 'expected productivity/fugu' "$dir/path.err" || rc=1
+grep -qx 'preserve staged port' "$portdir/sentinel" || rc=1
+ok "rollback: a wrong computed package path restores the prior port" "$rc"
+export FUGU_TEST_FULLPKGPATH=productivity/fugu
+
+if env FUGU_LOCAL_PORTDIR="$dir/unsafe" \
+    make -s -C "$repo" local-port >"$dir/unsafe.out" \
+    2>"$dir/unsafe.err"; then
+	rc=1
+else
+	rc=0
+fi
+grep -q 'must end in /mystuff/productivity/fugu' \
+    "$dir/unsafe.err" || rc=1
+ok "safety: destructive staging is limited to the local fugu pkgpath" "$rc"
+
+find "$portsdir/mystuff/productivity" \
+    \( -name 'fugu.new.*' -o -name 'fugu.old.*' \) -print \
+    >"$dir/leftovers"
+test ! -s "$dir/leftovers"
+ok "cleanup: no staging or backup directories survive" $?
+
+printf '%d checks, %d failures\n' "$checks" "$fails"
+test "$fails" -eq 0
blob - 6b3aca0ae076aa623a553cedcb7cab171b9baa2d
blob + 986ab49b4d8c0d3cce7f7e755d65787b5d36059f
--- regress/model_window/model_window_test.c
+++ regress/model_window/model_window_test.c
@@ -58,7 +58,7 @@ test_provider_metadata(void)
 static void
 test_effective_order(void)
 {
-	/* Operator override wins over selected metadata and the table. */
+	/* Owner override wins over selected metadata and the table. */
 	CHECK(model_window_effective(777777, 424242,
 	    "claude-known") == 777777);
 	/* The selected provider's value wins over a compiled known model. */
blob - 4e2bf6deaa28c9216497a9e3260ad6c005c3619a
blob + a4a2bd7e830d1d83a101d771581fd2deb6ab8f7a
--- regress/print/run.sh
+++ regress/print/run.sh
@@ -102,6 +102,16 @@ project_context no
 EOF
 }
 
+write_claude_conf() {
+	umask 077
+	cat >"$dir/fugu.conf" <<EOF
+model "claude-sonnet-4-6"
+provider "claude"
+oauth_token "claude-oauth-print-test"
+project_context no
+EOF
+}
+
 run_fugu() {
 	FUGU_CONF=$dir/fugu.conf FUGU_LIBEXEC=$dir/libexec/fugu \
 	    FUGU_CA_FILE=$dir/cert.pem FUGU_RETRY_BASE_MS=20 \
@@ -109,6 +119,14 @@ run_fugu() {
 	rc=$?
 }
 
+run_claude_fugu() {
+	FUGU_CONF=$dir/fugu.conf FUGU_LIBEXEC=$dir/libexec/fugu \
+	    FUGU_CA_FILE=$dir/cert.pem FUGU_RETRY_BASE_MS=20 \
+	    FUGU_REGRESS_CLAUDE_PORT=$stubport \
+	    "$fugu" "$@" </dev/null >"$dir/out" 2>"$dir/errout"
+	rc=$?
+}
+
 sessions=$HOME/.fugu/sessions
 
 # One final object, with hostile content escaped and NUL round-tripped.
@@ -299,6 +317,46 @@ ok "ndjson: tool turn exits 0" $([ "$rc" -eq 0 ]; echo
 ok "ndjson: taxonomy and aggregate 8/6 usage are stable" $?
 stop_stub
 
+# The experimental Claude subscription path remains inside the pure-C
+# Custodian: fixed request profile, bearer credential, and tool-name mapping
+# work through a complete tool round without an SDK subprocess.
+reset_home
+start_stub "CLAUDE_TOOL OK"
+write_claude_conf
+run_claude_fugu -p "use the Claude subscription tool" -o ndjson
+ok "claude-subscription: complete tool round exits 0" \
+    $([ "$rc" -eq 0 ]; echo $?)
+"$obj/jsoncheck" ndjson "$dir/out"
+ok "claude-subscription: canonical tool events retain NDJSON shape" $?
+grep -q '^2$' "$dir/reqcount" 2>/dev/null && \
+    grep -q 'TOOL-RESULT-1' "$dir/reqout" 2>/dev/null
+ok "claude-subscription: mapped tool result reaches the next request" $?
+grep -q 'POST /v1/messages?beta=true HTTP/1.1' "$dir/reqout" && \
+    grep -q 'Authorization: Bearer claude-oauth-print-test' \
+    "$dir/reqout" && \
+    grep -q 'anthropic-beta: .*oauth-2025-04-20' "$dir/reqout" && \
+    ! grep -qi '^x-api-key:' "$dir/reqout"
+ok "claude-subscription: fixed endpoint and bearer headers reach TLS" $?
+grep -q "You are Claude Code, Anthropic's official CLI for Claude." \
+    "$dir/reqout" && \
+    grep -q 'x-anthropic-billing-header:' "$dir/reqout" && \
+    grep -q '"name":"mcp_Read"' "$dir/reqout" && \
+    grep -q '"name":"mcp_Shell"' "$dir/reqout"
+ok "claude-subscription: identity, billing, and mapped tools are on wire" $?
+stop_stub
+
+reset_home
+start_stub "CLAUDE_REJECT"
+write_claude_conf
+run_claude_fugu -p "reject this compatibility profile" -o ndjson
+ok "claude-subscription: upstream rejection fails the Turn" \
+    $([ "$rc" -eq 1 ]; echo $?)
+"$obj/jsoncheck" error "$dir/out"
+ok "claude-subscription: rejection retains machine error shape" $?
+grep -q 'Claude subscription profile claude-code-2.1.185' "$dir/out"
+ok "claude-subscription: rejection identifies the active profile" $?
+stop_stub
+
 # Print mode uses the slash-command and Skill dispatcher.
 reset_home
 start_stub "OK OK"
blob - 1ef69866f1786c67f6d3d34dfa58491d9b6a69d5
blob + 7f6fb8c3799174dcb1de19f68bc1210c3a2c9bb4
--- regress/print/stub.c
+++ regress/print/stub.c
@@ -239,6 +239,19 @@ send_truncated_503(struct tls *conn)
 	(void)tls_write_all(conn, response, sizeof(response) - 1);
 }
 
+static void
+send_claude_rejection(struct tls *conn)
+{
+	static const char response[] =
+	    "HTTP/1.1 400 Bad Request\r\n"
+	    "content-type: application/json\r\n"
+	    "content-length: 92\r\n\r\n"
+	    "{\"type\":\"error\",\"error\":{\"type\":\"invalid_request_error\","
+	    "\"message\":\"compatibility rejected\"}}";
+
+	(void)tls_write_all(conn, response, sizeof(response) - 1);
+}
+
 /* Five body bytes per second stay inside the per-read timeout.  The client
  * must nevertheless close this attempt at its five-second aggregate cap. */
 static int
@@ -285,7 +298,7 @@ send_drip_503(struct tls *conn, int listener, struct t
 }
 
 static void
-send_tool(struct tls *conn, int number)
+send_tool(struct tls *conn, const char *name, int number)
 {
 	char	 sse[4096];
 	int	 n;
@@ -306,7 +319,7 @@ send_tool(struct tls *conn, int number)
 	    "event: content_block_start\n"
 	    "data: {\"type\":\"content_block_start\",\"index\":1,"
 	    "\"content_block\":{\"type\":\"tool_use\","
-	    "\"id\":\"toolu_%d\",\"name\":\"shell\"}}\n\n"
+	    "\"id\":\"toolu_%d\",\"name\":\"%s\"}}\n\n"
 	    "event: content_block_delta\n"
 	    "data: {\"type\":\"content_block_delta\",\"index\":1,"
 	    "\"delta\":{\"type\":\"input_json_delta\","
@@ -319,7 +332,7 @@ send_tool(struct tls *conn, int number)
 	    "\"stop_reason\":\"tool_use\"},"
 	    "\"usage\":{\"output_tokens\":4}}\n\n"
 	    "event: message_stop\n"
-	    "data: {\"type\":\"message_stop\"}\n\n", number, number);
+	    "data: {\"type\":\"message_stop\"}\n\n", number, name, number);
 	if (n < 0 || (size_t)n >= sizeof(sse))
 		errx(1, "tool fixture too large");
 	send_sse(conn, sse);
@@ -514,6 +527,8 @@ main(int argc, char *argv[])
 			send_reflected_http_error(conn, reflected_key);
 		} else if (reqlen > 0 && strcmp(argv[i], "TRUNC503") == 0)
 			send_truncated_503(conn);
+		else if (reqlen > 0 && strcmp(argv[i], "CLAUDE_REJECT") == 0)
+			send_claude_rejection(conn);
 		else if (reqlen > 0 && strcmp(argv[i], "DRIP503") == 0) {
 			if (i + 1 >= argc || strcmp(argv[i + 1], "OK") != 0)
 				errx(1, "DRIP503 requires a following OK scenario");
@@ -526,7 +541,9 @@ main(int argc, char *argv[])
 			send_sse(conn, sse_ok);
 		else if (reqlen > 0 && (strcmp(argv[i], "TOOL") == 0 ||
 		    strcmp(argv[i], "LOOP") == 0))
-			send_tool(conn, i + 1);
+			send_tool(conn, "shell", i + 1);
+		else if (reqlen > 0 && strcmp(argv[i], "CLAUDE_TOOL") == 0)
+			send_tool(conn, "mcp_Shell", i + 1);
 		else if (reqlen > 0 && strcmp(argv[i], "SLEEP_TOOL") == 0)
 			send_sleep_tool(conn, "shell", child_pidfile, i + 1);
 		else if (reqlen > 0 && strcmp(argv[i], "BG_TOOL") == 0)
blob - cd0e6e1e852cc3acca7ffa676c534dfcc90ee5f0
blob + fbde8b9464fceb0681f58f70fde07b85dd63e73e
--- regress/sandbox/harness.c
+++ regress/sandbox/harness.c
@@ -57,6 +57,7 @@
 #define TEST_BAD_HELLO_ID	(-2)
 #define TEST_BAD_PING		(-3)
 #define TEST_BAD_PING_ID	(-4)
+#define TEST_CLAUDE_MODELS	(-5)
 
 /* Blocking imsg read of one message of the expected type. */
 static int
@@ -140,6 +141,8 @@ action_of(const char *s)
 		return (TEST_BAD_PING);
 	if (strcmp(s, "bad_ping_id") == 0)
 		return (TEST_BAD_PING_ID);
+	if (strcmp(s, "claude_models") == 0)
+		return (TEST_CLAUDE_MODELS);
 	if (strcmp(s, "none") == 0)
 		return (0);
 	errx(1, "unknown action %s", s);
@@ -160,7 +163,8 @@ main(int argc, char *argv[])
 	struct hello_tty	 hello;
 	enum fugu_role		 role;
 	pid_t			 wpid;
-	int			 action, readonly = 0, nowrite = 0, status, alive;
+	int			 action, claude = 0, readonly = 0, nowrite = 0;
+	int			 status, alive;
 	const char		*expect, *rolename;
 	char			 byte = 'x';
 
@@ -174,6 +178,9 @@ main(int argc, char *argv[])
 	} else if (strcmp(rolename, "fugu-tool-ro") == 0) {
 		nowrite = 1;			/* allow_write no: the I6 gate */
 		rolename = "fugu-tool";
+	} else if (strcmp(rolename, "fugu-api-claude") == 0) {
+		claude = 1;
+		rolename = "fugu-api";
 	}
 	role = role_of(rolename);
 	action = action_of(argv[3]);
@@ -207,13 +214,27 @@ main(int argc, char *argv[])
 	if (role == ROLE_TTY) {
 		send_msg(&ibuf, FUGU_IMSG_HELLO, &hello, sizeof(hello));
 	} else if (role == ROLE_API) {
-		struct hello_api	 h;
+		struct {
+			struct hello_api h;
+			char key[16];
+		} p;
+		size_t len;
 
-		memset(&h, 0, sizeof(h));
-		strlcpy(h.host, "127.0.0.1", sizeof(h.host));
-		strlcpy(h.port, "443", sizeof(h.port));
-		strlcpy(h.path, "/v1/messages", sizeof(h.path));
-		send_msg(&ibuf, FUGU_IMSG_HELLO, &h, sizeof(h));
+		memset(&p, 0, sizeof(p));
+		p.h.provider_type = claude || action == TEST_CLAUDE_MODELS ?
+		    PROVIDER_CLAUDE : PROVIDER_ANTHROPIC;
+		strlcpy(p.h.host, claude || action == TEST_CLAUDE_MODELS ?
+		    "api.anthropic.com" : "127.0.0.1", sizeof(p.h.host));
+		strlcpy(p.h.port, "443", sizeof(p.h.port));
+		strlcpy(p.h.path, claude || action == TEST_CLAUDE_MODELS ?
+		    "/v1/messages?beta=true" : "/v1/messages", sizeof(p.h.path));
+		strlcpy(p.h.model, "claude-sonnet-4-6", sizeof(p.h.model));
+		len = sizeof(p.h);
+		if (claude || action == TEST_CLAUDE_MODELS) {
+			strlcpy(p.key, "fake-token", sizeof(p.key));
+			len += strlen(p.key);
+		}
+		send_msg(&ibuf, FUGU_IMSG_HELLO, &p, len);
 	} else if (role == ROLE_TOOL) {
 		struct hello_tool	 h;
 
@@ -241,6 +262,33 @@ main(int argc, char *argv[])
 		errx(2, "%s sent malformed READY", rolename);
 	imsg_free(&imsg);
 
+	if (action == TEST_CLAUDE_MODELS) {
+		struct model_ent me;
+		u_char p[sizeof(me) + 1 + FUGU_MODEL_ID_MAX];
+		size_t len, model_len = strlen("claude-sonnet-4-6");
+
+		send_msg(&ibuf, FUGU_IMSG_LIST_MODELS, NULL, 0);
+		if (expect_msg(&ibuf, FUGU_IMSG_MODEL, &imsg) == -1)
+			errx(2, "fugu-api did not list configured Claude model");
+		len = imsg_get_len(&imsg);
+		if (len != sizeof(me) + 1 + model_len ||
+		    imsg_get_data(&imsg, p, len) == -1)
+			errx(2, "malformed configured Claude model");
+		memcpy(&me, p, sizeof(me));
+		if (me.slot != 0 || me.context_window != 200000 ||
+		    p[sizeof(me)] != '\0' || memcmp(p + sizeof(me) + 1,
+		    "claude-sonnet-4-6", model_len) != 0)
+			errx(2, "wrong configured Claude model");
+		imsg_free(&imsg);
+		if (expect_msg(&ibuf, FUGU_IMSG_MODELS_END, &imsg) == -1 ||
+		    imsg_get_len(&imsg) != 0)
+			errx(2, "configured Claude model list did not end");
+		imsg_free(&imsg);
+		send_msg(&ibuf, FUGU_IMSG_SHUTDOWN, NULL, 0);
+		waitpid(kid.pid, &status, 0);
+		return (WIFEXITED(status) && WEXITSTATUS(status) == 0 ? 0 : 1);
+	}
+
 	if (action == TEST_BAD_PING || action == TEST_BAD_PING_ID) {
 		if (action == TEST_BAD_PING)
 			send_msg(&ibuf, FUGU_IMSG_PING, &byte, sizeof(byte));
blob - f85a09d29f9352c3f7a43139b226c91d8b268187
blob + dfeb858e0b2c2973fb5f97752d25b6efcb7d9212
--- regress/sandbox/run.sh
+++ regress/sandbox/run.sh
@@ -86,6 +86,7 @@ run_case() {
 # its legitimate handshake (I9).
 run_case fugu-tty  none ok -
 run_case fugu-api  none ok -
+run_case fugu-api  claude_models ok -
 run_case fugu-web  none ok -
 run_case fugu-tool none ok -
 run_case fugu-editor none ok -
@@ -103,6 +104,8 @@ run_case fugu-editor bad_ping_id  reject -
 # I1: the credential custodians cannot exec or open files.
 run_case fugu-api exec     dead execve
 run_case fugu-api open_etc dead open
+run_case fugu-api-claude exec     dead execve
+run_case fugu-api-claude open_etc dead open
 run_case fugu-web exec     dead execve
 run_case fugu-web open_etc dead open
 
@@ -114,7 +117,7 @@ run_case fugu-web open_tty dead open
 run_case fugu-tool connect dead socket
 
 # The editor room keeps exec but has no network: a compromise through
-# a compose yields the user's own shell access, never a data path out.
+# a compose yields the Owner's own shell access, never a data path out.
 run_case fugu-editor connect dead socket
 
 # I5-shaped: writes are confined to the tree; an in-tree write works,
blob - f7467fb0ab0b81a849b7917803259a10bb7ef278
blob + 7ae2ed186e50e0a6d768dd5f3dc328c8b131e11e
--- scripts/live-check
+++ scripts/live-check
@@ -131,8 +131,9 @@ usage: $prog credentialed
        $prog privileged
 
 credentialed (run as an ordinary user):
-  FUGU_LIVE_SECRET_FILES   required whitespace-separated provider, routed
-                           provider, and Kagi key-file paths
+  FUGU_LIVE_SECRET_FILES   required whitespace-separated direct-provider,
+                           routed-provider, and Kagi key-file paths; Claude
+                           OAuth is read securely from FUGU_LIVE_CONF
   FUGU_LIVE_BIN            fugu executable [/usr/local/bin/fugu]
   FUGU_LIVE_CONF           effective configuration [/etc/fugu.conf]
   FUGU_LIVE_LIBEXEC        optional development worker directory
@@ -148,8 +149,9 @@ credentialed (run as an ordinary user):
 
 privileged (run as root on a clean OpenBSD host):
   FUGU_LIVE_PACKAGE        required package path
-  FUGU_LIVE_CONF_SOURCE    required ready-to-install configuration; referenced
-                           key files must already exist at their final paths
+  FUGU_LIVE_CONF_SOURCE    required ready-to-install configuration; Claude
+                           OAuth is inline, while referenced key files must
+                           already exist at their final paths
   FUGU_LIVE_SECRET_FILES   required whitespace-separated final key-file paths
   FUGU_LIVE_USER           required ordinary test user, not in _fugu
   FUGU_LIVE_CONFIRM        required hostname:package-sha256 confirmation
@@ -160,11 +162,11 @@ privileged (run as root on a clean OpenBSD host):
 
 Examples:
   env FUGU_LIVE_SECRET_FILES='/etc/fugu.key /etc/fugu-work.key /etc/fugu-kagi.key' make live-check
-  doas env FUGU_LIVE_PACKAGE=/tmp/fugu-0.2.tgz \\
+  doas env FUGU_LIVE_PACKAGE=/tmp/fugu-0.3.tgz \\
       FUGU_LIVE_CONF_SOURCE=/root/fugu-live.conf \\
       FUGU_LIVE_SECRET_FILES='/etc/fugu.key /etc/fugu-work.key /etc/fugu-kagi.key' \\
       FUGU_LIVE_USER=alice \\
-      FUGU_LIVE_CONFIRM="\$(hostname):\$(sha256 -q /tmp/fugu-0.2.tgz)" \\
+      FUGU_LIVE_CONFIRM="\$(hostname):\$(sha256 -q /tmp/fugu-0.3.tgz)" \\
       make live-check-privileged
 EOF
 }
@@ -174,6 +176,43 @@ line_is()
 	grep -Fqx "$2" "$1" 2>/dev/null
 }
 
+# Extract every simple quoted oauth_token value. The release audit deliberately
+# rejects comments, whitespace, escapes, and macros on these lines so it can
+# prove exactly which secret patterns it scanned without printing them.
+oauth_extract_program='
+/^[ \t]*oauth_token[ \t]+/ {
+	if ($0 !~ /^[ \t]*oauth_token[ \t]+"[^"[:space:]]+"[ \t]*$/) {
+		bad = 1
+		next
+	}
+	value = $0
+	sub(/^[ \t]*oauth_token[ \t]+"/, "", value)
+	sub(/"[ \t]*$/, "", value)
+	if (value == "")
+		bad = 1
+	else {
+		print value
+		found = 1
+	}
+}
+END { if (bad || !found) exit 1 }
+'
+
+oauth_syslog_program='
+NR == FNR { keys[++nkeys] = $0; next }
+index($0, start) { copying = 1; saw_start = 1 }
+copying {
+	for (i = 1; i <= nkeys; i++)
+		if (index($0, keys[i])) leaked = 1
+}
+copying && index($0, end) { saw_end = 1; exit }
+END {
+	if (!saw_start || !saw_end) exit 2
+	if (leaked) exit 1
+	exit 0
+}
+'
+
 credentialed_run()
 {
 	(
@@ -185,6 +224,7 @@ credentialed_run()
 			exec env HOME="$live_home" FUGU_CONF=/nonexistent \
 			    FUGU_LIBEXEC=/nonexistent FUGU_CA_FILE=/nonexistent \
 			    FUGU_KAGI_HOST=127.0.0.1 FUGU_KAGI_PORT=1 \
+			    FUGU_REGRESS_CLAUDE_PORT=1 \
 			    "$live_bin" "$@" </dev/null
 		fi
 		if [ -n "$live_libexec" ]; then
@@ -221,6 +261,73 @@ secret_file_ok()
 	return 1
 }
 
+inline_oauth_ok()
+{
+	conf=$1
+	if [ -r "$conf" ]; then
+		LC_ALL=C awk "$oauth_extract_program" "$conf" >/dev/null 2>&1
+		return $?
+	fi
+	command -v "$live_doas" >/dev/null 2>&1 || return 1
+	"$live_doas" env LC_ALL=C awk "$oauth_extract_program" "$conf" \
+	    >/dev/null 2>&1
+}
+
+# Return 0 when every inline OAuth token is absent, 1 when leaked, and 2 when
+# the protected config or target could not be scanned. If doas is needed, token
+# extraction, matching, and deletion all happen inside that privileged shell.
+inline_oauth_absent()
+{
+	conf=$1
+	target=$2
+	scan='
+		umask 077
+		patterns=$(mktemp /tmp/fugu-live-oauth.XXXXXXXXXX) || exit 2
+		trap "rm -f $patterns" 0 1 2 3 15
+		LC_ALL=C awk "$1" "$2" >"$patterns" || exit 2
+		grep -R -F -q -f "$patterns" "$3" >/dev/null 2>&1
+		case $? in
+		0) exit 1 ;;
+		1) exit 0 ;;
+		*) exit 2 ;;
+		esac
+	'
+	if [ -r "$conf" ]; then
+		env LC_ALL=C sh -c "$scan" sh "$oauth_extract_program" \
+		    "$conf" "$target"
+		return $?
+	fi
+	command -v "$live_doas" >/dev/null 2>&1 || return 2
+	"$live_doas" env LC_ALL=C sh -c "$scan" sh \
+	    "$oauth_extract_program" "$conf" "$target"
+}
+
+inline_oauth_syslog_absent()
+{
+	conf=$1
+	start=$2
+	end=$3
+	scan='
+		umask 077
+		patterns=$(mktemp /tmp/fugu-live-oauth.XXXXXXXXXX) || exit 2
+		trap "rm -f $patterns" 0 1 2 3 15
+		LC_ALL=C awk "$1" "$3" >"$patterns" || exit 2
+		LC_ALL=C awk -v start="$5" -v end="$6" "$2" \
+		    "$patterns" "$4" >/dev/null 2>&1
+		exit $?
+	'
+	if [ -r "$conf" ] && [ -r "$live_syslog" ]; then
+		env LC_ALL=C sh -c "$scan" sh "$oauth_extract_program" \
+		    "$oauth_syslog_program" "$conf" "$live_syslog" \
+		    "$start" "$end"
+		return $?
+	fi
+	command -v "$live_doas" >/dev/null 2>&1 || return 2
+	"$live_doas" env LC_ALL=C sh -c "$scan" sh \
+	    "$oauth_extract_program" "$oauth_syslog_program" "$conf" \
+	    "$live_syslog" "$start" "$end"
+}
+
 # Set tool_result_line and prove one call has one correlated result and that
 # no unrelated tool call/result can satisfy the assertion.
 one_tool_result()
@@ -343,6 +450,10 @@ credentialed()
 		secret_file_ok "$key" ||
 		    die "$key must contain exactly one nonempty printable key line"
 	done
+	if [ "$expect_provider" = claude ]; then
+		inline_oauth_ok "$live_conf" ||
+		    die "$live_conf must contain a simple quoted oauth_token line"
+	fi
 	if [ -n "$expired_conf" ]; then
 		[ -f "$expired_conf" ] || die "$expired_conf is not a file"
 		[ -n "$expired_bin" ] && [ -x "$expired_bin" ] ||
@@ -390,8 +501,16 @@ credentialed()
 	ok "effective model is $expect_model" $?
 	line_is "$artifacts/config.out" "provider \"$expect_provider\""
 	ok "effective provider is $expect_provider" $?
-	line_is "$artifacts/config.out" "api_key <redacted>"
-	ok "default provider key is configured and redacted" $?
+	if [ "$expect_provider" = claude ]; then
+		line_is "$artifacts/config.out" "oauth_token <redacted>"
+		ok "default subscription token is configured and redacted" $?
+		grep -q '^# experimental Claude subscription profile ' \
+		    "$artifacts/config.out"
+		ok "dry-run identifies the experimental compatibility profile" $?
+	else
+		line_is "$artifacts/config.out" "api_key <redacted>"
+		ok "default provider key is configured and redacted" $?
+	fi
 	line_is "$artifacts/config.out" "kagi_token <redacted>"
 	ok "Kagi key is configured and redacted" $?
 	line_is "$artifacts/config.out" "web_search yes"
@@ -414,6 +533,25 @@ credentialed()
 	line_is "$artifacts/default.out" "live provider ok"
 	ok "default-provider turn returns the exact marker" $?
 
+	if [ "$expect_provider" = claude ]; then
+		live_workdir=$project
+		printf '%s\n' "LIVE-SUBSCRIPTION-TOOL-$$" >"$project/profile.txt"
+		credentialed_run -p \
+		    'Use read exactly once on profile.txt, then reply exactly: live subscription tool ok' \
+		    -o ndjson >"$artifacts/subscription-tool.out" \
+		    2>"$artifacts/subscription-tool.err"
+		rc=$?
+		ok_zero "subscription Tool turn exits zero" "$rc"
+		one_tool_result "$artifacts/subscription-tool.out" read false
+		ok "subscription profile completes one confined read Tool" $?
+		grep -q 'live subscription tool ok' \
+		    "$artifacts/subscription-tool.out"
+		ok "subscription profile completes after its Tool result" $?
+		grep -q 'experimental Claude subscription profile' \
+		    "$artifacts/subscription-tool.err"
+		ok "subscription startup warns that the profile is experimental" $?
+	fi
+
 	live_workdir=$project
 	subprompt="Call the agent tool exactly twice in the same response and no other tool. Label the calls live-a and live-b. live-a must read alpha.txt and then return exactly $report_a without quoting the file. live-b must read beta.txt and then return exactly $report_b without quoting the file. After both reports arrive, reply exactly: live subagents ok"
 	credentialed_run -p "$subprompt" -o ndjson \
@@ -537,6 +675,17 @@ credentialed()
 		rc=$?
 		ok "$(basename "$key") is absent from syslog" "$rc"
 	done
+	if [ "$expect_provider" = claude ]; then
+		inline_oauth_absent "$live_conf" "$artifacts"
+		rc=$?
+		ok "inline oauth_token is absent from terminal output" "$rc"
+		inline_oauth_absent "$live_conf" "$journals"
+		rc=$?
+		ok "inline oauth_token is absent from session journals" "$rc"
+		inline_oauth_syslog_absent "$live_conf" "$log_begin" "$log_end"
+		rc=$?
+		ok "inline oauth_token is absent from syslog" "$rc"
+	fi
 
 	finish
 }
@@ -582,8 +731,12 @@ privileged()
 	esac
 	if grep -Eq '^[[:space:]]*(api_key|anthropic_key|kagi_token)[[:space:]]+' \
 	    "$conf_source"; then
-		die "$conf_source contains an inline credential; use key files"
+		die "$conf_source contains an inline API/search credential; use key files"
 	fi
+	if [ "$expect_provider" = claude ]; then
+		inline_oauth_ok "$conf_source" ||
+		    die "$conf_source must contain a simple quoted oauth_token line"
+	fi
 	if grep -Eq '^[[:space:]]*include[[:space:]]+' "$conf_source"; then
 		die "$conf_source must be self-contained for the live audit"
 	fi
@@ -698,7 +851,7 @@ privileged()
 
 	install -o root -g _fugu -m 0640 "$conf_source" "$installed_conf"
 	rc=$?
-	ok "operator configuration installs root:_fugu mode 0640" "$rc"
+	ok "configuration installs root:_fugu mode 0640" "$rc"
 	[ "$rc" -eq 0 ] || { finish; return $?; }
 	chroot -u "$live_user" / /bin/test ! -r "$installed_conf"
 	ok "$live_user cannot read the admin configuration directly" $?
@@ -709,9 +862,15 @@ privileged()
 	ok_zero "ordinary user reads the protected configuration through setgid" \
 	    "$rc"
 	line_is "$tmp/user-config.out" "model \"$expect_model\"" &&
-	    line_is "$tmp/user-config.out" "provider \"$expect_provider\"" &&
-	    line_is "$tmp/user-config.out" "api_key <redacted>"
-	ok "ordinary-user dry run reports admin values with a redacted key" $?
+	    line_is "$tmp/user-config.out" "provider \"$expect_provider\""
+	rc=$?
+	if [ "$expect_provider" = claude ]; then
+		line_is "$tmp/user-config.out" "oauth_token <redacted>" || rc=1
+	else
+		line_is "$tmp/user-config.out" "api_key <redacted>" || rc=1
+	fi
+	ok "ordinary-user dry run reports admin values with a redacted credential" \
+	    "$rc"
 
 	user_tmp=$tmp/user
 	mkdir "$user_tmp"
blob - /dev/null
blob + 2c33c809da98c115eba26dbb5e8a50ed40a24892 (mode 755)
--- /dev/null
+++ scripts/check
@@ -0,0 +1,359 @@
+#!/bin/sh
+#
+# Copyright (c) 2026 Isaac <isaac@itm.works>
+#
+# Permission to use, copy, modify, and distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+# The clean-room source gate. Successful output is one line per phase and
+# regression suite; VERBOSE=1 adds the complete live build and test transcript.
+
+set -u
+umask 077
+
+prog=${0##*/}
+root=$(cd "$(dirname "$0")/.." && pwd)
+make_cmd=${FUGU_CHECK_MAKE:-make}
+cc_cmd=${CC:-cc}
+
+case ${VERBOSE-} in
+'')	verbose=0 ;;
+1)	verbose=1 ;;
+*)	printf '%s: VERBOSE must be unset or exactly 1\n' "$prog" >&2
+	exit 2
+	;;
+esac
+
+command -v "$make_cmd" >/dev/null 2>&1 || {
+	printf '%s: %s is not executable\n' "$prog" "$make_cmd" >&2
+	exit 2
+}
+command -v "$cc_cmd" >/dev/null 2>&1 || {
+	printf '%s: %s is not executable\n' "$prog" "$cc_cmd" >&2
+	exit 2
+}
+[ "$(uname -s)" = OpenBSD ] || {
+	printf '%s: check is OpenBSD-only\n' "$prog" >&2
+	exit 2
+}
+
+tmp=$(mktemp -d /tmp/fugu-check.XXXXXXXXXX) || {
+	printf '%s: cannot create temporary directory\n' "$prog" >&2
+	exit 2
+}
+started=$(date +%s)
+serial=0
+current_pid=
+tee_pid=
+fifo=
+session_cmd=$tmp/check-session
+current_label=
+current_rerun=
+line_open=0
+
+cleanup()
+{
+	rc=$?
+	set +e
+	trap - EXIT HUP INT TERM
+	if [ -n "$current_pid" ]; then
+		kill -TERM "-$current_pid" 2>/dev/null
+		wait "$current_pid" 2>/dev/null
+	fi
+	if [ -n "$tee_pid" ]; then
+		kill -TERM "$tee_pid" 2>/dev/null
+		wait "$tee_pid" 2>/dev/null
+	fi
+	[ -n "$fifo" ] && rm -f "$fifo"
+	rm -rf "$tmp"
+	exit "$rc"
+}
+
+interrupted()
+{
+	signal=$1
+	status=$2
+	trap - HUP INT TERM
+	if [ -n "$current_pid" ]; then
+		kill -s "$signal" "-$current_pid" 2>/dev/null || :
+	fi
+	if [ -n "$tee_pid" ]; then
+		kill -TERM "$tee_pid" 2>/dev/null || :
+	fi
+	if [ "$line_open" -eq 1 ]; then
+		printf 'INTERRUPTED\n'
+	elif [ -n "$current_label" ]; then
+		printf 'INTERRUPTED %s\n' "$current_label"
+	else
+		printf 'INTERRUPTED make check\n'
+	fi
+	if [ -n "$current_rerun" ]; then
+		printf 'Rerun: %s\n' "$current_rerun"
+	fi
+	exit "$status"
+}
+
+trap cleanup EXIT
+trap 'interrupted HUP 129' HUP
+trap 'interrupted INT 130' INT
+trap 'interrupted TERM 143' TERM
+
+log_ends_newline()
+{
+	[ ! -s "$1" ] && return 0
+	last=$(tail -c 1 "$1" | od -An -tu1 | tr -d ' ')
+	[ "$last" = 10 ] || [ "$last" = 010 ]
+}
+
+replay_log()
+{
+	label=$1
+	log=$2
+	printf '%s\n' "----- $label output -----"
+	cat "$log"
+	log_ends_newline "$log" || printf '\n'
+	printf '%s\n' "----- end $label output -----"
+}
+
+begin_item()
+{
+	current_label=$1
+	current_rerun=$2
+	if [ "$verbose" -eq 1 ]; then
+		printf '===> %s\n' "$current_label"
+		line_open=0
+	else
+		printf '===> %s ... ' "$current_label"
+		line_open=1
+	fi
+}
+
+success_item()
+{
+	label=$1
+	detail=$2
+	if [ "$verbose" -eq 1 ]; then
+		printf 'SUCCESS %s%s\n' "$label" "$detail"
+	else
+		printf 'SUCCESS%s\n' "$detail"
+	fi
+	line_open=0
+	current_label=
+	current_rerun=
+}
+
+fail_item()
+{
+	label=$1
+	reason=$2
+	log=$3
+	rc=$4
+	if [ "$verbose" -eq 1 ]; then
+		log_ends_newline "$log" || printf '\n'
+		printf 'FAIL %s\n' "$label"
+	else
+		printf 'FAIL\n'
+		replay_log "$label" "$log"
+	fi
+	[ -n "$reason" ] && printf 'Reason: %s\n' "$reason"
+	[ -n "$current_rerun" ] && printf 'Rerun: %s\n' "$current_rerun"
+	exit "$rc"
+}
+
+run_logged()
+{
+	log=$1
+	shift
+	: >"$log" || return 2
+
+	if [ "$verbose" -eq 0 ]; then
+		"$session_cmd" "$@" >"$log" 2>&1 &
+		current_pid=$!
+		wait "$current_pid"
+		rc=$?
+		current_pid=
+		return "$rc"
+	fi
+
+	fifo=$tmp/fifo.$serial
+	mkfifo "$fifo" || return 2
+	tee "$log" <"$fifo" &
+	tee_pid=$!
+	"$session_cmd" "$@" >"$fifo" 2>&1 &
+	current_pid=$!
+	wait "$current_pid"
+	rc=$?
+	current_pid=
+	wait "$tee_pid"
+	tee_rc=$?
+	tee_pid=
+	rm -f "$fifo"
+	fifo=
+	[ "$rc" -ne 0 ] && return "$rc"
+	return "$tee_rc"
+}
+
+new_log()
+{
+	serial=$((serial + 1))
+	log=$tmp/output.$serial
+}
+
+run_stage()
+{
+	label=$1
+	rerun=$2
+	check_warnings=$3
+	shift 3
+	new_log
+	begin_item "$label" "$rerun"
+	if run_logged "$log" "$@"; then
+		rc=0
+	else
+		rc=$?
+		fail_item "$label" "command exited $rc" "$log" "$rc"
+	fi
+	if [ "$check_warnings" -eq 1 ] &&
+	    LC_ALL=C grep -Eiq '(^|[[:space:]])warning:' "$log"; then
+		fail_item "$label" "compiler warning in a successful build" \
+		    "$log" 1
+	fi
+	success_item "$label" ""
+}
+
+incomplete_proof()
+{
+	LC_ALL=C awk '
+	/^(SKIP|SKIPPED|DISABLED|XFAIL|XPASS)([[:space:]]|$)/ {
+		print
+		exit
+	}
+	/^(EXPECTED_FAIL|UNEXPECTED_PASS)([[:space:]]|$)/ {
+		print
+		exit
+	}
+	/^not ok([[:space:]]|$)/ {
+		print
+		exit
+	}
+	/^FAIL([[:space:]]|$)/ {
+		print
+		exit
+	}
+	/^[0-9]+ checks, [1-9][0-9]* failures([[:space:]]|$)/ {
+		print
+		exit
+	}' "$1"
+}
+
+count_checks()
+{
+	LC_ALL=C awk '
+	/^ok[[:space:]]+[0-9]+([[:space:]]|$)/ { checks++ }
+	END { print checks + 0 }' "$1"
+}
+
+run_suite()
+{
+	suite=$1
+	new_log
+	run="make -C regress/$suite regress"
+	begin_item "$suite" "$run"
+	if run_logged "$log" "$make_cmd" -C "$root/regress/$suite" regress; then
+		rc=0
+	else
+		rc=$?
+		fail_item "$suite" "suite exited $rc" "$log" "$rc"
+	fi
+	missing=$(incomplete_proof "$log")
+	if [ -n "$missing" ]; then
+		fail_item "$suite" "incomplete proof: $missing" "$log" 1
+	fi
+	if LC_ALL=C grep -Eiq '(^|[[:space:]])warning:' "$log"; then
+		fail_item "$suite" "compiler warning in regression suite" \
+		    "$log" 1
+	fi
+	checks=$(count_checks "$log")
+	case "$checks" in
+	1)	detail=" (1 check)" ;;
+	*)	detail=" ($checks checks)" ;;
+	esac
+	success_item "$suite" "$detail"
+	suite_count=$((suite_count + 1))
+	total_checks=$((total_checks + checks))
+}
+
+format_duration()
+{
+	seconds=$1
+	hours=$((seconds / 3600))
+	minutes=$(((seconds % 3600) / 60))
+	seconds=$((seconds % 60))
+	if [ "$hours" -gt 0 ]; then
+		printf '%dh %dm %ds' "$hours" "$minutes" "$seconds"
+	elif [ "$minutes" -gt 0 ]; then
+		printf '%dm %ds' "$minutes" "$seconds"
+	else
+		printf '%ds' "$seconds"
+	fi
+}
+
+bootstrap_log=$tmp/bootstrap
+if ! "$cc_cmd" -Wall -Wextra -Werror -o "$session_cmd" \
+    "$root/scripts/check-session.c" >"$bootstrap_log" 2>&1; then
+	printf '%s: cannot build terminal-detach helper\n' "$prog" >&2
+	cat "$bootstrap_log" >&2
+	exit 2
+fi
+if LC_ALL=C grep -Eiq '(^|[[:space:]])warning:' "$bootstrap_log"; then
+	printf '%s: compiler warning in terminal-detach helper\n' "$prog" >&2
+	cat "$bootstrap_log" >&2
+	exit 1
+fi
+
+run_stage "Cleaning" "make cleandir" 0 \
+    "$make_cmd" -C "$root" cleandir
+run_stage "Creating object directories" "make obj" 0 \
+    "$make_cmd" -C "$root" obj
+run_stage "Building" "make" 1 "$make_cmd" -C "$root"
+
+suites=$("$make_cmd" -s -C "$root/regress" -V SUBDIR) || {
+	printf '%s: cannot read regress SUBDIR\n' "$prog" >&2
+	exit 2
+}
+[ -n "$suites" ] || {
+	printf '%s: regress SUBDIR is empty\n' "$prog" >&2
+	exit 2
+}
+
+printf '===> Running regression suites\n'
+suite_count=0
+total_checks=0
+for suite in $suites; do
+	case "$suite" in
+	''|*[!A-Za-z0-9_-]*)
+		printf '%s: unsafe regression suite name: %s\n' \
+		    "$prog" "$suite" >&2
+		exit 2
+		;;
+	esac
+	run_suite "$suite"
+done
+
+finished=$(date +%s)
+elapsed=$(format_duration $((finished - started)))
+suite_word=suites
+check_word=checks
+[ "$suite_count" -eq 1 ] && suite_word=suite
+[ "$total_checks" -eq 1 ] && check_word=check
+printf '===> All checks passed (%d %s, %d %s; %s)\n' \
+    "$suite_count" "$suite_word" "$total_checks" "$check_word" "$elapsed"
blob - /dev/null
blob + a3acb2290e40e148be962f6287059e9c9762bb14 (mode 644)
--- /dev/null
+++ scripts/check-session.c
@@ -0,0 +1,35 @@
+/*
+ * Copyright (c) 2026 Isaac <isaac@itm.works>
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+/*
+ * Run a check command without a controlling terminal.  Some regression
+ * proofs deliberately trip pledge(2); its kernel diagnostic is written
+ * directly to the controlling terminal and cannot be redirected by sh(1).
+ */
+
+#include <err.h>
+#include <unistd.h>
+
+int
+main(int argc, char *argv[])
+{
+	if (argc < 2)
+		errx(2, "missing command");
+	if (setsid() == -1)
+		err(1, "setsid");
+	execvp(argv[1], argv + 1);
+	err(127, "%s", argv[1]);
+}
blob - /dev/null
blob + 84bbe48767d5de1590a9b2ef217f1f31a0979b74 (mode 755)
--- /dev/null
+++ scripts/local-port
@@ -0,0 +1,199 @@
+#!/bin/sh
+#
+# Copyright (c) 2026 Isaac <isaac@itm.works>
+#
+# Permission to use, copy, modify, and distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+# Stage the current clean Git commit as a checksummed local OpenBSD port.
+# Packaging and installation deliberately remain bsd.port.mk operations.
+
+set -eu
+umask 022
+
+prog=${0##*/}
+root=$(cd "$(dirname "$0")/.." && pwd)
+port_source=$root/port/productivity/fugu
+port_dir=${FUGU_LOCAL_PORTDIR:-/usr/ports/mystuff/productivity/fugu}
+port_make=${FUGU_LOCAL_PORT_MAKE:-make}
+doas=${FUGU_LOCAL_PORT_DOAS:-doas}
+expected_pkgpath=productivity/fugu
+
+tmp=
+stage=
+backup=
+activated=0
+had_old=0
+keep=0
+
+die()
+{
+	printf '%s: %s\n' "$prog" "$*" >&2
+	exit 1
+}
+
+cleanup()
+{
+	rc=$?
+	set +e
+	trap - EXIT HUP INT TERM
+
+	if [ "$keep" -eq 0 ]; then
+		if [ "$activated" -eq 1 ]; then
+			rm -rf "$port_dir"
+		fi
+		if [ "$had_old" -eq 1 ] && [ -e "$backup" ]; then
+			if mv "$backup" "$port_dir"; then
+				backup=
+			else
+				printf '%s: previous port retained at %s\n' \
+				    "$prog" "$backup" >&2
+			fi
+		fi
+	fi
+	if [ -n "$stage" ] && [ -e "$stage" ]; then
+		rm -rf "$stage"
+	fi
+	if [ "$keep" -eq 1 ] && [ -n "$backup" ] && [ -e "$backup" ]; then
+		rm -rf "$backup"
+	fi
+	if [ -n "$tmp" ] && [ -d "$tmp" ]; then
+		rm -rf "$tmp"
+	fi
+	exit "$rc"
+}
+
+trap cleanup EXIT
+trap 'exit 1' HUP INT TERM
+
+run_privileged()
+{
+	"$doas" "$@"
+}
+
+port_value()
+{
+	name=$1
+	value=$("$port_make" -s -C "$port_dir" "show=$name") ||
+	    die "cannot query $name from $port_dir"
+	[ -n "$value" ] || die "$name is empty in $port_dir"
+	case "$value" in
+	*'
+'*)	die "$name has more than one value in $port_dir" ;;
+	esac
+	printf '%s\n' "$value"
+}
+
+[ "$(uname -s)" = OpenBSD ] || die "local-port is OpenBSD-only"
+[ "$(id -u)" -ne 0 ] || die "run local-port as an ordinary user"
+command -v git >/dev/null 2>&1 || die "git is not installed"
+command -v "$port_make" >/dev/null 2>&1 ||
+    die "$port_make is not executable"
+command -v "$doas" >/dev/null 2>&1 || die "$doas is not executable"
+[ -d "$port_source" ] || die "$port_source is missing"
+
+case "$port_dir" in
+/*/mystuff/productivity/fugu)	;;
+*)	die "local port path must end in /mystuff/productivity/fugu" ;;
+esac
+
+git_root=$(git -C "$root" rev-parse --show-toplevel 2>/dev/null) ||
+    die "$root is not a Git working tree"
+[ "$git_root" = "$root" ] || die "$root is not the Git working-tree root"
+if [ -n "$(git -C "$root" status --porcelain --untracked-files=normal)" ]; then
+	die "working tree is not clean; commit or remove every change first"
+fi
+commit=$(git -C "$root" rev-parse --verify HEAD) ||
+    die "cannot resolve the current commit"
+
+owner=$(id -un)
+group=$(id -gn)
+port_parent=${port_dir%/fugu}
+run_privileged install -d -o "$owner" -g "$group" "$port_parent" ||
+    die "cannot prepare $port_parent"
+
+stage=${port_dir}.new.$$
+backup=${port_dir}.old.$$
+[ ! -e "$stage" ] || die "$stage already exists"
+[ ! -e "$backup" ] || die "$backup already exists"
+cp -R "$port_source" "$stage" || die "cannot stage the maintained port"
+
+if [ -e "$port_dir" ]; then
+	mv "$port_dir" "$backup" || die "cannot preserve the previous local port"
+	had_old=1
+fi
+mv "$stage" "$port_dir" || die "cannot activate the staged local port"
+activated=1
+stage=
+
+fullpkgpath=$(port_value FULLPKGPATH)
+[ "$fullpkgpath" = "$expected_pkgpath" ] ||
+    die "staged package path is $fullpkgpath, expected $expected_pkgpath"
+fullpkgname=$(port_value FULLPKGNAME)
+distname=$(port_value DISTNAME)
+distfiles=$(port_value DISTFILES)
+extract_sufx=$(port_value EXTRACT_SUFX)
+fulldistdir=$(port_value FULLDISTDIR)
+checksum_file=$(port_value CHECKSUM_FILE)
+wrkdir=$(port_value WRKDIR)
+package_repository=$(port_value PACKAGE_REPOSITORY)
+plist_repository=$(port_value PLIST_REPOSITORY)
+
+case "$distname" in
+''|*[!A-Za-z0-9._+-]*)	die "unsafe DISTNAME: $distname" ;;
+esac
+archive=${distname}${extract_sufx}
+[ "$distfiles" = "$archive" ] ||
+    die "DISTFILES must contain exactly $archive"
+case "$fulldistdir" in
+/*)	;;
+*)	die "FULLDISTDIR is not absolute: $fulldistdir" ;;
+esac
+case "$checksum_file" in
+/*)	;;
+*)	checksum_file=$port_dir/$checksum_file ;;
+esac
+
+tmp=$(mktemp -d /tmp/fugu-local-port.XXXXXXXXXX) ||
+    die "cannot create a temporary directory"
+archive_tmp=$tmp/$archive
+git -C "$root" archive --format=tar.gz --prefix="${distname}/" \
+    -o "$archive_tmp" "$commit" || die "cannot archive $commit"
+digest=$(sha256 -q "$archive_tmp") || die "cannot checksum $archive_tmp"
+
+if [ ! -d "$fulldistdir" ]; then
+	run_privileged install -d -m 0755 "$fulldistdir" ||
+	    die "cannot create $fulldistdir"
+fi
+run_privileged install -m 0644 "$archive_tmp" "$fulldistdir/$archive" ||
+    die "cannot install $archive in $fulldistdir"
+
+"$port_make" -C "$port_dir" makesum || die "make makesum failed"
+[ -s "$checksum_file" ] || die "make makesum did not create $checksum_file"
+"$port_make" -C "$port_dir" checksum || die "make checksum failed"
+
+keep=1
+printf 'Local port prepared from committed source.\n'
+printf '  commit:            %s\n' "$commit"
+printf '  package path:       %s\n' "$fullpkgpath"
+printf '  package name:       %s\n' "$fullpkgname"
+printf '  distfile:           %s\n' "$fulldistdir/$archive"
+printf '  distfile sha256:    %s\n' "$digest"
+printf '  port directory:     %s\n' "$port_dir"
+printf '  work directory:     %s\n' "$wrkdir"
+printf '  package repository: %s\n' "$package_repository"
+printf '  plist repository:   %s\n' "$plist_repository"
+printf '\nNext, for an unpublished same-version rebuild:\n'
+printf '  make -C %s clean=all\n' "$port_dir"
+printf '  make -C %s package\n' "$port_dir"
+printf '  doas make -C %s reinstall\n' "$port_dir"
+printf 'Use the ports install target instead of reinstall for a first install.\n'
blob - /dev/null
blob + 706f4c4b243661ec15dea116a9232e5f2d6652fd (mode 644)
--- /dev/null
+++ src/common/claude_sub.h
@@ -0,0 +1,47 @@
+/*
+ * Copyright (c) 2026 Isaac <isaac@itm.works>
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ */
+
+#ifndef CLAUDE_SUB_H
+#define CLAUDE_SUB_H
+
+#include <sys/types.h>
+
+#include <stdint.h>
+
+#include "buf.h"
+#include "msg.h"
+
+#define CLAUDE_SUB_PROFILE_ID	"claude-code-2.1.185"
+#define CLAUDE_SUB_PROFILE_DATE	"2026-07-16"
+#define CLAUDE_SUB_VERSION	"2.1.185"
+#define CLAUDE_SUB_HOST		"api.anthropic.com"
+#define CLAUDE_SUB_PORT		"443"
+#define CLAUDE_SUB_PATH		"/v1/messages?beta=true"
+
+/* Canonical Projection input to the experimental subscription profile. */
+struct claude_sub_req {
+	const char		*model;
+	int64_t			 max_tokens;
+	const char		*system;
+	size_t			 systemlen;
+	const struct tool_def	*tools;
+	int			 ntools;
+	const struct msglist	*conv;
+	const struct msg		*suffix;
+	int			 cache;
+	int			 cache_tail_msg;
+};
+
+int	 claude_sub_build_request(struct buf *, const struct claude_sub_req *);
+int	 claude_sub_tool_map(char *, size_t, const char *);
+int	 claude_sub_tool_unmap(char *, size_t, const char *);
+const char *claude_sub_betas(const char *);
+int	 claude_sub_build_headers(char *, size_t, const char *, const char *,
+	    const char *, const char *);
+
+#endif /* CLAUDE_SUB_H */
blob - /dev/null
blob + 6616f350f10a1bb3070b90b7a55b041de7443df0 (mode 644)
--- /dev/null
+++ src/common/claude_sub_profile.c
@@ -0,0 +1,111 @@
+/*
+ * Copyright (c) 2026 Isaac <isaac@itm.works>
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+/*
+ * Volatile, observed Claude Code compatibility data.  This is deliberately
+ * one small module: changing the profile requires fixture and live validation.
+ */
+
+#include <sys/types.h>
+
+#include <ctype.h>
+#include <stddef.h>
+#include <string.h>
+
+#include "claude_sub.h"
+
+static const char base_betas[] =
+    "claude-code-20250219,oauth-2025-04-20,"
+    "interleaved-thinking-2025-05-14,"
+    "prompt-caching-scope-2026-01-05,"
+    "context-management-2025-06-27,advisor-tool-2026-03-01,"
+    "thinking-token-count-2026-05-13,"
+    "extended-cache-ttl-2025-04-11,effort-2025-11-24";
+
+static const char haiku_betas[] =
+    "claude-code-20250219,oauth-2025-04-20,"
+    "prompt-caching-scope-2026-01-05,"
+    "context-management-2025-06-27,advisor-tool-2026-03-01,"
+    "thinking-token-count-2026-05-13,"
+    "extended-cache-ttl-2025-04-11,effort-2025-11-24";
+
+static int
+contains_ci(const char *s, const char *needle)
+{
+	size_t i, j, nlen;
+
+	nlen = strlen(needle);
+	for (i = 0; s[i] != '\0'; i++) {
+		for (j = 0; j < nlen; j++) {
+			if (s[i + j] == '\0' ||
+			    tolower((unsigned char)s[i + j]) !=
+			    tolower((unsigned char)needle[j]))
+				break;
+		}
+		if (j == nlen)
+			return (1);
+	}
+	return (0);
+}
+
+const char *
+claude_sub_betas(const char *model)
+{
+	if (model != NULL && contains_ci(model, "haiku"))
+		return (haiku_betas);
+	return (base_betas);
+}
+
+int
+claude_sub_tool_map(char *out, size_t outsz, const char *name)
+{
+	size_t i, len;
+
+	if (out == NULL || outsz == 0 || name == NULL ||
+	    (len = strlen(name)) == 0 || len > SIZE_MAX - 5 ||
+	    outsz <= len + 4 || name[0] < 'a' || name[0] > 'z')
+		return (-1);
+	for (i = 0; i < len; i++)
+		if (!((name[i] >= 'a' && name[i] <= 'z') ||
+		    (name[i] >= '0' && name[i] <= '9') || name[i] == '_'))
+			return (-1);
+	memcpy(out, "mcp_", 4);
+	out[4] = (char)toupper((unsigned char)name[0]);
+	memcpy(out + 5, name + 1, len);
+	out[len + 4] = '\0';
+	return (0);
+}
+
+int
+claude_sub_tool_unmap(char *out, size_t outsz, const char *name)
+{
+	size_t i, len;
+
+	if (out == NULL || outsz == 0 || name == NULL ||
+	    strncmp(name, "mcp_", 4) != 0 ||
+	    (len = strlen(name + 4)) == 0 || outsz <= len ||
+	    name[4] < 'A' || name[4] > 'Z')
+		return (-1);
+	for (i = 4; name[i] != '\0'; i++)
+		if (!((name[i] >= 'A' && name[i] <= 'Z') ||
+		    (name[i] >= 'a' && name[i] <= 'z') ||
+		    (name[i] >= '0' && name[i] <= '9') || name[i] == '_'))
+			return (-1);
+	out[0] = (char)tolower((unsigned char)name[4]);
+	memcpy(out + 1, name + 5, len - 1);
+	out[len] = '\0';
+	return (0);
+}
blob - /dev/null
blob + 93cd88cd099465bc503179dd8e3792574bfa30d6 (mode 644)
--- /dev/null
+++ src/common/claude_sub_req.c
@@ -0,0 +1,394 @@
+/*
+ * Copyright (c) 2026 Isaac <isaac@itm.works>
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+/*
+ * Credential-free Claude Subscription request shaping.  The canonical
+ * Projection remains untouched; only the serialized provider view carries
+ * the compatibility identity, billing attribution, and relocated instructions.
+ */
+
+#include <sys/queue.h>
+#include <sys/types.h>
+
+#include <sha2.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <string.h>
+
+#include "buf.h"
+#include "json.h"
+#include "msg.h"
+#include "claude_sub.h"
+
+#define BILLING_SALT	"59cf53e54c78"
+#define TOOL_WIRE_MAX	256
+
+static const char identity[] =
+    "You are Claude Code, Anthropic's official CLI for Claude.";
+
+static void
+emit_cache_control(struct json_out *jo)
+{
+	json_key(jo, "cache_control");
+	json_obj_begin(jo);
+	json_key(jo, "type");
+	json_cstr(jo, "ephemeral");
+	json_obj_end(jo);
+}
+
+static const struct block *
+first_user_text(const struct claude_sub_req *r)
+{
+	const struct msg *m;
+	const struct block *b;
+
+	TAILQ_FOREACH(m, r->conv, entry) {
+		if (m->role != ROLE_USER)
+			continue;
+		TAILQ_FOREACH(b, &m->blocks, entry)
+			if (b->type == BLOCK_TEXT)
+				return (b);
+	}
+	if (r->suffix != NULL && r->suffix->role == ROLE_USER)
+		TAILQ_FOREACH(b, &r->suffix->blocks, entry)
+			if (b->type == BLOCK_TEXT)
+				return (b);
+	return (NULL);
+}
+
+static void
+digest_prefix(const void *data, size_t len, char *out, size_t n)
+{
+	char hex[SHA256_DIGEST_STRING_LENGTH];
+
+	SHA256Data(data, len, hex);
+	memcpy(out, hex, n);
+	out[n] = '\0';
+	explicit_bzero(hex, sizeof(hex));
+}
+
+static int
+utf8_next(const u_char *s, size_t len, uint32_t *cp, size_t *used)
+{
+	uint32_t v;
+
+	if (len == 0)
+		return (-1);
+	if (s[0] < 0x80) {
+		*cp = s[0];
+		*used = 1;
+		return (0);
+	}
+	if (s[0] >= 0xc2 && s[0] <= 0xdf && len >= 2 &&
+	    (s[1] & 0xc0) == 0x80) {
+		*cp = ((uint32_t)(s[0] & 0x1f) << 6) | (s[1] & 0x3f);
+		*used = 2;
+		return (0);
+	}
+	if (s[0] >= 0xe0 && s[0] <= 0xef && len >= 3 &&
+	    (s[1] & 0xc0) == 0x80 && (s[2] & 0xc0) == 0x80) {
+		v = ((uint32_t)(s[0] & 0x0f) << 12) |
+		    ((uint32_t)(s[1] & 0x3f) << 6) | (s[2] & 0x3f);
+		if (v < 0x800 || (v >= 0xd800 && v <= 0xdfff))
+			return (-1);
+		*cp = v;
+		*used = 3;
+		return (0);
+	}
+	if (s[0] >= 0xf0 && s[0] <= 0xf4 && len >= 4 &&
+	    (s[1] & 0xc0) == 0x80 && (s[2] & 0xc0) == 0x80 &&
+	    (s[3] & 0xc0) == 0x80) {
+		v = ((uint32_t)(s[0] & 0x07) << 18) |
+		    ((uint32_t)(s[1] & 0x3f) << 12) |
+		    ((uint32_t)(s[2] & 0x3f) << 6) | (s[3] & 0x3f);
+		if (v < 0x10000 || v > 0x10ffff)
+			return (-1);
+		*cp = v;
+		*used = 4;
+		return (0);
+	}
+	return (-1);
+}
+
+/* Append one JavaScript UTF-16 code unit as Node encodes it for hashing. */
+static void
+append_js_unit(struct buf *out, const char *text, size_t textlen, size_t target)
+{
+	static const u_char replacement[] = { 0xef, 0xbf, 0xbd };
+	const u_char *p = (const u_char *)text;
+	size_t off, units, used, width;
+	uint32_t cp;
+	int valid;
+
+	off = units = 0;
+	while (off < textlen) {
+		valid = utf8_next(p + off, textlen - off, &cp, &used) == 0;
+		if (!valid) {
+			cp = 0xfffd;
+			used = 1;
+		}
+		width = cp > 0xffff ? 2 : 1;
+		if (target >= units && target < units + width) {
+			/* Selecting either half of a supplementary character
+			 * yields an unpaired surrogate; Node encodes U+FFFD. */
+			if (!valid || width == 2)
+				buf_add(out, replacement, sizeof(replacement));
+			else
+				buf_add(out, p + off, used);
+			return;
+		}
+		units += width;
+		off += used;
+	}
+	buf_addc(out, '0');
+}
+
+static int
+billing_header(const struct claude_sub_req *r, char *out, size_t outsz)
+{
+	const struct block *first;
+	struct buf material;
+	char cch[6], suffix[4];
+	const char *text;
+	size_t i, textlen;
+	int n;
+
+	first = first_user_text(r);
+	text = first != NULL ? first->text : "";
+	textlen = first != NULL ? first->textlen : 0;
+	digest_prefix(text, textlen, cch, 5);
+	buf_init(&material);
+	buf_addstr(&material, BILLING_SALT);
+	for (i = 0; i < 3; i++) {
+		static const size_t positions[] = { 4, 7, 20 };
+
+		append_js_unit(&material, text, textlen, positions[i]);
+	}
+	buf_addstr(&material, CLAUDE_SUB_VERSION);
+	digest_prefix(material.data, material.len, suffix, 3);
+	buf_freezero(&material);
+	n = snprintf(out, outsz, "x-anthropic-billing-header: "
+	    "cc_version=%s.%s; cc_entrypoint=sdk-cli; cch=%s;",
+	    CLAUDE_SUB_VERSION, suffix, cch);
+	explicit_bzero(cch, sizeof(cch));
+	explicit_bzero(suffix, sizeof(suffix));
+	return (n < 0 || (size_t)n >= outsz ? -1 : 0);
+}
+
+static int
+validate_request(const struct claude_sub_req *r)
+{
+	const struct msg *m;
+	const struct block *b;
+	char mapped[TOOL_WIRE_MAX], previous[TOOL_WIRE_MAX];
+	int i, j;
+
+	if (r == NULL || r->model == NULL || r->model[0] == '\0' ||
+	    r->max_tokens < 1 || r->conv == NULL ||
+	    (r->system == NULL && r->systemlen != 0) || r->ntools < 0 ||
+	    (r->ntools != 0 && r->tools == NULL))
+		return (-1);
+	for (i = 0; i < r->ntools; i++) {
+		if (r->tools[i].name == NULL || r->tools[i].description == NULL ||
+		    r->tools[i].schema == NULL || claude_sub_tool_map(mapped,
+		    sizeof(mapped), r->tools[i].name) == -1)
+			return (-1);
+		for (j = 0; j < i; j++) {
+			if (claude_sub_tool_map(previous, sizeof(previous),
+			    r->tools[j].name) == -1)
+				return (-1);
+			if (strcmp(mapped, previous) == 0)
+				return (-1);
+		}
+	}
+	TAILQ_FOREACH(m, r->conv, entry)
+		TAILQ_FOREACH(b, &m->blocks, entry)
+			if (b->type == BLOCK_TOOL_USE &&
+			    claude_sub_tool_map(mapped, sizeof(mapped),
+			    b->tool_name) == -1)
+				return (-1);
+	if (r->suffix != NULL)
+		TAILQ_FOREACH(b, &r->suffix->blocks, entry)
+			if (b->type == BLOCK_TOOL_USE &&
+			    claude_sub_tool_map(mapped, sizeof(mapped),
+			    b->tool_name) == -1)
+				return (-1);
+	if (r->systemlen != 0 && first_user_text(r) == NULL)
+		return (-1);
+	return (0);
+}
+
+static void
+emit_block(struct json_out *jo, const struct block *b, int cache)
+{
+	char mapped[TOOL_WIRE_MAX];
+
+	json_obj_begin(jo);
+	switch (b->type) {
+	case BLOCK_TEXT:
+		json_key(jo, "type");
+		json_cstr(jo, "text");
+		json_key(jo, "text");
+		json_str(jo, b->text, b->textlen);
+		break;
+	case BLOCK_TOOL_USE:
+		(void)claude_sub_tool_map(mapped, sizeof(mapped), b->tool_name);
+		json_key(jo, "type");
+		json_cstr(jo, "tool_use");
+		json_key(jo, "id");
+		json_cstr(jo, b->tool_id);
+		json_key(jo, "name");
+		json_cstr(jo, mapped);
+		json_key(jo, "input");
+		if (b->tool_input_len != 0)
+			json_raw(jo, b->tool_input, b->tool_input_len);
+		else {
+			json_obj_begin(jo);
+			json_obj_end(jo);
+		}
+		break;
+	case BLOCK_TOOL_RESULT:
+		json_key(jo, "type");
+		json_cstr(jo, "tool_result");
+		json_key(jo, "tool_use_id");
+		json_cstr(jo, b->tool_use_id);
+		json_key(jo, "content");
+		json_str(jo, b->result, b->result_len);
+		if (b->is_error) {
+			json_key(jo, "is_error");
+			json_bool(jo, 1);
+		}
+		break;
+	}
+	if (cache)
+		emit_cache_control(jo);
+	json_obj_end(jo);
+}
+
+static void
+emit_message(struct json_out *jo, const struct msg *m,
+    const struct claude_sub_req *r, int prepend_system, int cache)
+{
+	const struct block *b, *last;
+
+	last = TAILQ_LAST(&m->blocks, blocklist);
+	json_obj_begin(jo);
+	json_key(jo, "role");
+	json_cstr(jo, m->role == ROLE_USER ? "user" : "assistant");
+	json_key(jo, "content");
+	json_arr_begin(jo);
+	if (prepend_system && r->systemlen != 0) {
+		json_obj_begin(jo);
+		json_key(jo, "type");
+		json_cstr(jo, "text");
+		json_key(jo, "text");
+		json_str(jo, r->system, r->systemlen);
+		if (r->cache)
+			emit_cache_control(jo);
+		json_obj_end(jo);
+	}
+	TAILQ_FOREACH(b, &m->blocks, entry)
+		emit_block(jo, b, cache && b == last);
+	json_arr_end(jo);
+	json_obj_end(jo);
+}
+
+int
+claude_sub_build_request(struct buf *out, const struct claude_sub_req *r)
+{
+	struct json_out jo;
+	const struct msg *m;
+	struct buf body;
+	char billing[256], mapped[TOOL_WIRE_MAX];
+	int i, mi, moved;
+
+	if (out == NULL || validate_request(r) == -1 ||
+	    billing_header(r, billing, sizeof(billing)) == -1)
+		return (-1);
+	buf_init(&body);
+	json_out_init(&jo, &body);
+	json_obj_begin(&jo);
+	json_key(&jo, "model");
+	json_cstr(&jo, r->model);
+	json_key(&jo, "max_tokens");
+	json_num(&jo, r->max_tokens);
+	json_key(&jo, "stream");
+	json_bool(&jo, 1);
+
+	json_key(&jo, "system");
+	json_arr_begin(&jo);
+	json_obj_begin(&jo);
+	json_key(&jo, "type");
+	json_cstr(&jo, "text");
+	json_key(&jo, "text");
+	json_cstr(&jo, billing);
+	json_obj_end(&jo);
+	json_obj_begin(&jo);
+	json_key(&jo, "type");
+	json_cstr(&jo, "text");
+	json_key(&jo, "text");
+	json_cstr(&jo, identity);
+	json_obj_end(&jo);
+	json_arr_end(&jo);
+
+	if (r->ntools > 0) {
+		json_key(&jo, "tools");
+		json_arr_begin(&jo);
+		for (i = 0; i < r->ntools; i++) {
+			(void)claude_sub_tool_map(mapped, sizeof(mapped),
+			    r->tools[i].name);
+			json_obj_begin(&jo);
+			json_key(&jo, "name");
+			json_cstr(&jo, mapped);
+			json_key(&jo, "description");
+			json_cstr(&jo, r->tools[i].description);
+			json_key(&jo, "input_schema");
+			json_raw(&jo, r->tools[i].schema,
+			    strlen(r->tools[i].schema));
+			if (r->cache && i == r->ntools - 1)
+				emit_cache_control(&jo);
+			json_obj_end(&jo);
+		}
+		json_arr_end(&jo);
+	}
+
+	json_key(&jo, "messages");
+	json_arr_begin(&jo);
+	mi = 0;
+	moved = 0;
+	TAILQ_FOREACH(m, r->conv, entry) {
+		int prepend;
+
+		prepend = !moved && m->role == ROLE_USER;
+		emit_message(&jo, m, r, prepend,
+		    r->cache && mi == r->cache_tail_msg);
+		if (prepend)
+			moved = 1;
+		mi++;
+	}
+	if (r->suffix != NULL) {
+		int prepend = !moved && r->suffix->role == ROLE_USER;
+
+		emit_message(&jo, r->suffix, r, prepend, 0);
+	}
+	json_arr_end(&jo);
+	json_obj_end(&jo);
+
+	buf_add(out, body.data, body.len);
+	buf_free(&body);
+	explicit_bzero(billing, sizeof(billing));
+	return (0);
+}
blob - /dev/null
blob + eeef5fa01e297704991139ce09a14baefdddfe2b (mode 644)
--- /dev/null
+++ src/common/claude_sub_wire.c
@@ -0,0 +1,89 @@
+/*
+ * Copyright (c) 2026 Isaac <isaac@itm.works>
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+/* Credentialed HTTP portion of the experimental subscription profile. */
+
+#include <sys/types.h>
+
+#include <ctype.h>
+#include <stddef.h>
+#include <stdio.h>
+#include <string.h>
+
+#include "claude_sub.h"
+
+#if defined(__aarch64__)
+#define STAINLESS_ARCH "arm64"
+#else
+#define STAINLESS_ARCH "x64"
+#endif
+
+static int
+uuid_ok(const char *s)
+{
+	size_t i;
+
+	if (s == NULL || strlen(s) != 36)
+		return (0);
+	for (i = 0; i < 36; i++) {
+		if (i == 8 || i == 13 || i == 18 || i == 23) {
+			if (s[i] != '-')
+				return (0);
+		} else if (!isxdigit((unsigned char)s[i]))
+			return (0);
+	}
+	return (1);
+}
+
+int
+claude_sub_build_headers(char *out, size_t outsz, const char *token,
+    const char *model, const char *session_id, const char *request_id)
+{
+	const char *betas;
+	int n;
+
+	if (out == NULL || outsz == 0 || token == NULL || token[0] == '\0' ||
+	    model == NULL || model[0] == '\0' ||
+	    strpbrk(token, "\r\n") != NULL || !uuid_ok(session_id) ||
+	    !uuid_ok(request_id))
+		return (-1);
+	betas = claude_sub_betas(model);
+	n = snprintf(out, outsz,
+	    "Authorization: Bearer %s\r\n"
+	    "anthropic-version: 2023-06-01\r\n"
+	    "anthropic-beta: %s\r\n"
+	    "anthropic-dangerous-direct-browser-access: true\r\n"
+	    "x-app: cli\r\n"
+	    "user-agent: claude-cli/%s (external, sdk-cli)\r\n"
+	    "x-client-request-id: %s\r\n"
+	    "x-claude-code-session-id: %s\r\n"
+	    "x-stainless-arch: " STAINLESS_ARCH "\r\n"
+	    "x-stainless-lang: js\r\n"
+	    "x-stainless-os: openbsd\r\n"
+	    "x-stainless-package-version: 0.81.0\r\n"
+	    "x-stainless-retry-count: 0\r\n"
+	    "x-stainless-runtime: node\r\n"
+	    "x-stainless-runtime-version: v24.18.0\r\n"
+	    "x-stainless-timeout: 600\r\n"
+	    "content-type: application/json\r\n",
+	    token, betas, CLAUDE_SUB_VERSION, request_id, session_id);
+	if (n < 0 || (size_t)n >= outsz) {
+		if (outsz > 0)
+			explicit_bzero(out, outsz);
+		return (-1);
+	}
+	return (0);
+}
blob - 23ad5acb4b4a838aa9315c06b0bee54dbccfb07a
blob + ee85e6d908de2b3df1b362acd6f7684bd8e75634
--- src/common/model_window.c
+++ src/common/model_window.c
@@ -51,7 +51,7 @@ model_window_parse(const struct json *j, int obj)
 	return (0);
 }
 
-/* behavior.md section 3: operator, selected metadata, table, unknown. */
+/* behavior.md section 3: Owner, selected metadata, table, unknown. */
 int64_t
 model_window_effective(int64_t context_limit, int64_t provider_window,
     const char *model)
blob - e4bfb3fa6050306c035eb09fe1ca0d61ba252be2
blob + 148f52fe64099fd853425940c49bbd069d86533e
--- src/fugu/Makefile
+++ src/fugu/Makefile
@@ -1,7 +1,8 @@
 PROG=	fugu
 SRCS=	main.c coord.c priv.c conf.c parse.y \
 	journal.c sysprompt.c tooldefs.c agentcfg.c skills.c output.c \
-	turn_txn.c turn_mechanics.c anthropic_req.c openai_req.c generation.c msg.c \
+	turn_txn.c turn_mechanics.c anthropic_req.c claude_sub_req.c \
+	claude_sub_profile.c openai_req.c generation.c msg.c \
 	json.c model_window.c buf.c imsgev.c log.c xmalloc.c
 MAN=	fugu.1 fugu.conf.5
 MANDIR=	${PREFIX}/man/man
blob - 68b1f752c19e603f2bbbd52fdd4da4e85438c5e3
blob + f75838f02999b3b04c5a10fdc5e85933ff3d1ccf
--- src/fugu/agentcfg.c
+++ src/fugu/agentcfg.c
@@ -178,7 +178,7 @@ build_schema(struct agentcfg *ac, int lead_slot, int l
 	(void)lead;
 	clear_choices(ac);
 
-	/* The immutable operator palette: implicit then named providers. */
+	/* The immutable Owner palette: implicit then named providers. */
 	for (i = 0; i < ac->nproviders; i++)
 		add_choice(ac, (int)i, ac->providers[i].type,
 		    ac->providers[i].model);
blob - 38ca754b0192c4ceca0635d18080e4a3dd9d1c66
blob + 129f9dc150dbe5a4ddabaca2d2ffee525e490860
--- src/fugu/agentcfg.h
+++ src/fugu/agentcfg.h
@@ -37,7 +37,7 @@ struct agentcfg_route {
 };
 
 /*
- * Snapshot the operator-configured provider/model palette.  Call this
+ * Snapshot the Owner-configured provider/model palette.  Call this
  * before /model is allowed to replace cf->model.  The snapshot deliberately
  * copies no endpoint or credential.  Credential availability is retained as
  * a boolean so an unusable implicit slot cannot enter the model palette.
blob - 6a0994b0cb12955946bb152626196a6a45a10b78
blob + 6c0eb932abd0c346232a70ae4cdd3c73f791f51d
--- src/fugu/conf.c
+++ src/fugu/conf.c
@@ -26,6 +26,7 @@
 
 #include "log.h"
 #include "xmalloc.h"
+#include "claude_sub.h"
 #include "conf.h"
 
 #define REDACTED	"<redacted>"
@@ -60,6 +61,8 @@ provider_free(struct fugu_provider *p)
 	free(p->api_path);
 	if (p->api_key != NULL)
 		freezero(p->api_key, strlen(p->api_key));
+	if (p->oauth_token != NULL)
+		freezero(p->oauth_token, strlen(p->oauth_token));
 	free(p);
 }
 
@@ -83,6 +86,8 @@ conf_free(struct fugu_conf *c)
 		freezero(c->api_key, strlen(c->api_key));
 	if (c->anthropic_key != NULL)
 		freezero(c->anthropic_key, strlen(c->anthropic_key));
+	if (c->oauth_token != NULL)
+		freezero(c->oauth_token, strlen(c->oauth_token));
 	if (c->kagi_token != NULL)
 		freezero(c->kagi_token, strlen(c->kagi_token));
 	free(c->http_allow);
@@ -112,6 +117,10 @@ conf_wipe_secrets(struct fugu_conf *c)
 		freezero(c->anthropic_key, strlen(c->anthropic_key));
 		c->anthropic_key = NULL;
 	}
+	if (c->oauth_token != NULL) {
+		freezero(c->oauth_token, strlen(c->oauth_token));
+		c->oauth_token = NULL;
+	}
 	if (c->kagi_token != NULL) {
 		freezero(c->kagi_token, strlen(c->kagi_token));
 		c->kagi_token = NULL;
@@ -121,46 +130,63 @@ conf_wipe_secrets(struct fugu_conf *c)
 			freezero(p->api_key, strlen(p->api_key));
 			p->api_key = NULL;
 		}
+		if (p->oauth_token != NULL) {
+			freezero(p->oauth_token, strlen(p->oauth_token));
+			p->oauth_token = NULL;
+		}
 	}
 }
 
 const char *
 conf_provider_name(int type)
 {
-	return (type == PROVIDER_OPENAI ? "openai" : "anthropic");
+	if (type == PROVIDER_OPENAI)
+		return ("openai");
+	if (type == PROVIDER_CLAUDE)
+		return ("claude");
+	return ("anthropic");
 }
 
 static const char *
 type_api_host(int type)
 {
-	return (type == PROVIDER_OPENAI ? "api.openai.com" :
-	    "api.anthropic.com");
+	if (type == PROVIDER_OPENAI)
+		return ("api.openai.com");
+	if (type == PROVIDER_CLAUDE)
+		return (CLAUDE_SUB_HOST);
+	return ("api.anthropic.com");
 }
 
 static const char *
 type_api_path(int type)
 {
-	return (type == PROVIDER_OPENAI ? "/v1/chat/completions" :
-	    "/v1/messages");
+	if (type == PROVIDER_OPENAI)
+		return ("/v1/chat/completions");
+	if (type == PROVIDER_CLAUDE)
+		return (CLAUDE_SUB_PATH);
+	return ("/v1/messages");
 }
 
 const char *
 conf_api_host(const struct fugu_conf *c)
 {
-	return (c->api_host != NULL ? c->api_host :
+	return (c->provider_type != PROVIDER_CLAUDE && c->api_host != NULL ?
+	    c->api_host :
 	    type_api_host(c->provider_type));
 }
 
 const char *
 conf_api_port(const struct fugu_conf *c)
 {
-	return (c->api_port != NULL ? c->api_port : "443");
+	return (c->provider_type != PROVIDER_CLAUDE && c->api_port != NULL ?
+	    c->api_port : CLAUDE_SUB_PORT);
 }
 
 const char *
 conf_api_path(const struct fugu_conf *c)
 {
-	return (c->api_path != NULL ? c->api_path :
+	return (c->provider_type != PROVIDER_CLAUDE && c->api_path != NULL ?
+	    c->api_path :
 	    type_api_path(c->provider_type));
 }
 
@@ -169,19 +195,22 @@ conf_api_path(const struct fugu_conf *c)
 const char *
 conf_prov_host(const struct fugu_provider *p)
 {
-	return (p->api_host != NULL ? p->api_host : type_api_host(p->type));
+	return (p->type != PROVIDER_CLAUDE && p->api_host != NULL ?
+	    p->api_host : type_api_host(p->type));
 }
 
 const char *
 conf_prov_port(const struct fugu_provider *p)
 {
-	return (p->api_port != NULL ? p->api_port : "443");
+	return (p->type != PROVIDER_CLAUDE && p->api_port != NULL ?
+	    p->api_port : CLAUDE_SUB_PORT);
 }
 
 const char *
 conf_prov_path(const struct fugu_provider *p)
 {
-	return (p->api_path != NULL ? p->api_path : type_api_path(p->type));
+	return (p->type != PROVIDER_CLAUDE && p->api_path != NULL ?
+	    p->api_path : type_api_path(p->type));
 }
 
 /*
@@ -192,6 +221,8 @@ conf_prov_path(const struct fugu_provider *p)
 const char *
 conf_effective_key(const struct fugu_conf *c)
 {
+	if (c->provider_type == PROVIDER_CLAUDE)
+		return (c->oauth_token);
 	if (c->api_key != NULL)
 		return (c->api_key);
 	if (c->provider_type == PROVIDER_ANTHROPIC)
@@ -221,15 +252,22 @@ conf_dump(const struct fugu_conf *c, FILE *f)
 	host = active != NULL ? conf_prov_host(active) : conf_api_host(c);
 	port = active != NULL ? conf_prov_port(active) : conf_api_port(c);
 	path = active != NULL ? conf_prov_path(active) : conf_api_path(c);
-	key = active != NULL ? active->api_key : conf_effective_key(c);
+	key = active != NULL ? (active->type == PROVIDER_CLAUDE ?
+	    active->oauth_token : active->api_key) : conf_effective_key(c);
 
 	fprintf(f, "model \"%s\"\n", c->model);
 	fprintf(f, "provider \"%s\"\n", conf_provider_name(c->provider_type));
-	fprintf(f, "api_host \"%s\"\n", host);
-	fprintf(f, "api_port \"%s\"\n", port);
-	fprintf(f, "api_path \"%s\"\n", path);
-	fprintf(f, "api_key %s\n", key != NULL ?
-	    REDACTED : "(not set)");
+	if (c->provider_type != PROVIDER_CLAUDE) {
+		fprintf(f, "api_host \"%s\"\n", host);
+		fprintf(f, "api_port \"%s\"\n", port);
+		fprintf(f, "api_path \"%s\"\n", path);
+	}
+	fprintf(f, "%s %s\n", c->provider_type == PROVIDER_CLAUDE ?
+	    "oauth_token" : "api_key", key != NULL ? REDACTED : "(not set)");
+	if (c->provider_type == PROVIDER_CLAUDE)
+		fprintf(f, "# experimental Claude subscription profile %s "
+		    "(snapshot %s)\n", CLAUDE_SUB_PROFILE_ID,
+		    CLAUDE_SUB_PROFILE_DATE);
 	fprintf(f, "kagi_token %s\n", c->kagi_token != NULL ?
 	    REDACTED : "(not set)");
 	fprintf(f, "max_tokens %lld\n", (long long)c->max_tokens);
@@ -265,13 +303,16 @@ conf_dump(const struct fugu_conf *c, FILE *f)
 		fprintf(f, "\ttype \"%s\"\n", conf_provider_name(p->type));
 		if (p->model != NULL)
 			fprintf(f, "\tmodel \"%s\"\n", p->model);
-		fprintf(f, "\tapi_host \"%s\"\n", p->api_host != NULL ?
-		    p->api_host : type_api_host(p->type));
-		fprintf(f, "\tapi_port \"%s\"\n", p->api_port != NULL ?
-		    p->api_port : "443");
-		fprintf(f, "\tapi_path \"%s\"\n", p->api_path != NULL ?
-		    p->api_path : type_api_path(p->type));
-		fprintf(f, "\tapi_key %s\n", REDACTED);
+		if (p->type != PROVIDER_CLAUDE) {
+			fprintf(f, "\tapi_host \"%s\"\n", p->api_host != NULL ?
+			    p->api_host : type_api_host(p->type));
+			fprintf(f, "\tapi_port \"%s\"\n", p->api_port != NULL ?
+			    p->api_port : "443");
+			fprintf(f, "\tapi_path \"%s\"\n", p->api_path != NULL ?
+			    p->api_path : type_api_path(p->type));
+		}
+		fprintf(f, "\t%s %s\n", p->type == PROVIDER_CLAUDE ?
+		    "oauth_token" : "api_key", REDACTED);
 		fprintf(f, "}\n");
 	}
 }
blob - b37a0b622352a35e2a8ded6404f13e6ccbb9447e
blob + 4f704363a3ed8dae87743361ec043cbd9f0813cd
--- src/fugu/conf.h
+++ src/fugu/conf.h
@@ -42,6 +42,7 @@ struct fugu_provider {
 	char				*api_port;
 	char				*api_path;
 	char				*api_key;	/* secret */
+	char				*oauth_token;	/* Claude subscription secret */
 };
 TAILQ_HEAD(fugu_providers, fugu_provider);
 
@@ -79,6 +80,7 @@ struct fugu_conf {
 	char			*api_path;
 	char			*api_key;	/* secret; NULL: unset */
 	char			*anthropic_key;	/* secret; legacy synonym */
+	char			*oauth_token;	/* Claude subscription secret */
 	char			*kagi_token;	/* secret; NULL: unset */
 	char			*http_allow;	/* NULL: tool withheld */
 	char			*http_block;
blob - 96f26a9a5cdab0c9f448a8f33a38a90f4a50eb76
blob + ad716e87ff862050b2394139462bf2d884011c2b
--- src/fugu/coord.c
+++ src/fugu/coord.c
@@ -42,6 +42,7 @@
 #include "model_window.h"
 #include "msg.h"
 #include "anthropic.h"
+#include "claude_sub.h"
 #include "openai.h"
 #include "imsgev.h"
 #include "conf.h"
@@ -67,7 +68,7 @@
 #define COMPACT_INSTR \
 "Write continuation notes to yourself that fully replace this "	\
 "conversation's history. Preserve, concisely and faithfully: the "	\
-"goals and the user's intent; every constraint and requirement; the "	\
+"goals and the Owner's intent; every constraint and requirement; the "	\
 "decisions made and the rationale behind them; the current state of "	\
 "the work and what remains; the key file paths, identifiers, and "	\
 "commands involved; and every open question. Write it addressed to "	\
@@ -722,6 +723,42 @@ send_tool_hello(struct coord *c, struct imsgev *iev, i
 	imsgev_send(iev, FUGU_IMSG_HELLO, 0, &h, sizeof(h));
 }
 
+static int
+claude_regress_port(const char **out)
+{
+	const char	*errstr, *port;
+
+	*out = NULL;
+	if (issetugid() || (port = getenv("FUGU_REGRESS_CLAUDE_PORT")) == NULL)
+		return (0);
+	(void)strtonum(port, 1, 65535, &errstr);
+	if (errstr != NULL)
+		return (-1);
+	*out = port;
+	return (1);
+}
+
+static int
+hello_endpoint(int type, const char *src_host, const char *src_port,
+    const char *src_path, char *host, size_t hostsz, char *port,
+    size_t portsz, char *path, size_t pathsz)
+{
+	const char	*regress_port;
+	int		 rc;
+
+	if (type == PROVIDER_CLAUDE &&
+	    (rc = claude_regress_port(&regress_port)) != 0) {
+		if (rc == -1)
+			fatalx("invalid FUGU_REGRESS_CLAUDE_PORT");
+		src_host = "127.0.0.1";
+		src_port = regress_port;
+		src_path = CLAUDE_SUB_PATH;
+	}
+	return (strlcpy(host, src_host, hostsz) >= hostsz ||
+	    strlcpy(port, src_port, portsz) >= portsz ||
+	    strlcpy(path, src_path, pathsz) >= pathsz ? -1 : 0);
+}
+
 static void
 send_hello(struct coord *c, enum fugu_role role)
 {
@@ -746,12 +783,11 @@ send_hello(struct coord *c, enum fugu_role role)
 
 		memset(&h, 0, sizeof(h));
 		h.provider_type = c->conf->provider_type;
-		if (strlcpy(h.host, conf_api_host(c->conf), sizeof(h.host)) >=
-		    sizeof(h.host) ||
-		    strlcpy(h.port, conf_api_port(c->conf), sizeof(h.port)) >=
-		    sizeof(h.port) ||
-		    strlcpy(h.path, conf_api_path(c->conf), sizeof(h.path)) >=
-		    sizeof(h.path))
+		if (hello_endpoint(h.provider_type, conf_api_host(c->conf),
+		    conf_api_port(c->conf), conf_api_path(c->conf), h.host,
+		    sizeof(h.host), h.port, sizeof(h.port), h.path,
+		    sizeof(h.path)) == -1 || strlcpy(h.model, c->conf->model,
+		    sizeof(h.model)) >= sizeof(h.model))
 			fatalx("default provider endpoint is too long");
 		key = conf_effective_key(c->conf);
 		if (key != NULL && strlen(key) > FUGU_SECRET_MAX)
@@ -772,22 +808,22 @@ send_hello(struct coord *c, enum fugu_role role)
 			memset(&hp, 0, sizeof(hp));
 			hp.type = fp->type;
 			if (strlcpy(hp.name, fp->name, sizeof(hp.name)) >=
-			    sizeof(hp.name) ||
-			    strlcpy(hp.host, conf_prov_host(fp), sizeof(hp.host)) >=
-			    sizeof(hp.host) ||
-			    strlcpy(hp.port, conf_prov_port(fp), sizeof(hp.port)) >=
-			    sizeof(hp.port) ||
-			    strlcpy(hp.path, conf_prov_path(fp), sizeof(hp.path)) >=
-			    sizeof(hp.path))
+			    sizeof(hp.name) || hello_endpoint(hp.type,
+			    conf_prov_host(fp), conf_prov_port(fp),
+			    conf_prov_path(fp), hp.host, sizeof(hp.host), hp.port,
+			    sizeof(hp.port), hp.path, sizeof(hp.path)) == -1 ||
+			    (fp->model != NULL && strlcpy(hp.model, fp->model,
+			    sizeof(hp.model)) >= sizeof(hp.model)))
 				fatalx("provider %s endpoint is too long", fp->name);
-			if (fp->api_key != NULL &&
-			    strlen(fp->api_key) > FUGU_SECRET_MAX)
+			key = fp->type == PROVIDER_CLAUDE ? fp->oauth_token :
+			    fp->api_key;
+			if (key != NULL && strlen(key) > FUGU_SECRET_MAX)
 				fatalx("provider %s key is too long", fp->name);
 
 			buf_init(&b);
 			buf_add(&b, &hp, sizeof(hp));
-			if (fp->api_key != NULL)
-				buf_add(&b, fp->api_key, strlen(fp->api_key));
+			if (key != NULL)
+				buf_add(&b, key, strlen(key));
 			imsgev_send(iev, FUGU_IMSG_PROV, 0, b.data, b.len);
 			buf_freezero(&b);
 		}
@@ -2132,6 +2168,9 @@ send_request(struct coord *c, struct buf *body, uint32
 	    body->data, body->len);
 	memset(&end, 0, sizeof(end));
 	end.provider_slot = provider_slot;
+	if (strlcpy(end.model, c->conf->model, sizeof(end.model)) >=
+	    sizeof(end.model))
+		fatalx("provider model is too long");
 	imsgev_send(iev, FUGU_IMSG_REQUEST_END, id, &end,
 	    sizeof(end));
 }
@@ -2332,6 +2371,9 @@ agent_dispatch(int fd, short event, void *arg)
 				fatalx("bad subagent provider request end");
 			memset(&end, 0, sizeof(end));
 			end.provider_slot = a->provider_slot;
+			if (strlcpy(end.model, a->model, sizeof(end.model)) >=
+			    sizeof(end.model))
+				fatalx("subagent provider model is too long");
 			imsgev_send(&c->iev[ROLE_API], FUGU_IMSG_REQUEST_END,
 			    a->api_id, &end, sizeof(end));
 			a->api_assembling = 0;
@@ -2627,6 +2669,22 @@ build_provider_request(struct coord *c, struct buf *bo
 		req.conv = messages;
 		req.suffix = suffix;
 		openai_build_request(body, &req);
+	} else if (c->conf->provider_type == PROVIDER_CLAUDE) {
+		struct claude_sub_req	 req;
+
+		memset(&req, 0, sizeof(req));
+		req.model = c->conf->model;
+		req.max_tokens = c->conf->max_tokens;
+		req.system = (const char *)c->sysfull.data;
+		req.systemlen = c->sysfull.len;
+		req.tools = tools;
+		req.ntools = ntools;
+		req.conv = messages;
+		req.suffix = suffix;
+		req.cache = 1;
+		req.cache_tail_msg = cache_tail_msg;
+		if (claude_sub_build_request(body, &req) == -1)
+			fatalx("cannot build Claude subscription request");
 	} else {
 		struct anthropic_req	 req;
 
@@ -3085,7 +3143,7 @@ agent_system(struct coord *c, struct buf *out)
 	    "self-contained task you were given. Explore and verify with the "
 	    "available read-only tools; you cannot modify files or start another "
 	    "agent. Return a concise final report with evidence, uncertainty, and "
-	    "useful file:line references. Do not address the user directly.";
+	    "useful file:line references. Do not address the Owner directly.";
 
 	buf_reset(out);
 	buf_addstr(out, base);
@@ -3919,7 +3977,7 @@ line_command(struct coord *c, const char *line, int *t
 		return (0);
 	}
 
-	/* /name [text]: a skill fires its body as the user's message,
+	/* /name [text]: a skill fires its body as the Owner's message,
 	 * with any trailing text appended as a second paragraph (S4) */
 	{
 		const char		*rest = line + 1, *sp = strchr(rest, ' ');
@@ -4123,7 +4181,7 @@ run_curses(struct coord *c)
 
 		/*
 		 * A steer that the turn never delivered (the model asked
-		 * for no further tools) is still the user's message and
+		 * for no further tools) is still the Owner's message and
 		 * was already taken off the tty's queue: it runs as the
 		 * immediate next turn, ahead of the auto-submits.
 		 */
@@ -4226,6 +4284,19 @@ static const struct {
 	{ ROLE_EDIT, "fugu-editor", 1 },	/* the editor needs the tty */
 };
 
+static int
+claude_subscription_configured(const struct fugu_conf *cf)
+{
+	const struct fugu_provider *fp;
+
+	if (cf->provider_type == PROVIDER_CLAUDE)
+		return (1);
+	TAILQ_FOREACH(fp, &cf->providers, entry)
+		if (fp->type == PROVIDER_CLAUDE)
+			return (1);
+	return (0);
+}
+
 int
 coord_run(struct fugu_conf *cf, const char *prompt, size_t promptlen,
 	    size_t dispatchlen, const char *resume_id, const char *argv0,
@@ -4360,7 +4431,19 @@ coord_run(struct fugu_conf *cf, const char *prompt, si
 	turn_jobs.commit = turn_jobs_commit;
 	turn_jobs.abort = turn_jobs_abort;
 	c.turn = turn_txn_new(c.journal, &seed, &turn_jobs);
+	if (claude_subscription_configured(cf)) {
+		static const char warning[] =
+		    "experimental Claude subscription profile "
+		    CLAUDE_SUB_PROFILE_ID " (snapshot " CLAUDE_SUB_PROFILE_DATE
+		    "): unofficial; it may stop working "
+		    "or trigger account enforcement";
 
+		if (c.ui)
+			emit_note(&c, "%s", warning);
+		else
+			fprintf(stderr, "fugu: warning: %s\n", warning);
+	}
+
 	if (prompt != NULL) {
 		signal_set(&c.sigint_ev, SIGINT, cancel_signal, &c);
 		if (signal_add(&c.sigint_ev, NULL) == -1)
blob - 5973654b6acc47a01ff59f5b6554a413238a662b
blob + 2811160f11ee550272b36ff18b2e241e843b186d
--- src/fugu/fugu.1
+++ src/fugu/fugu.1
@@ -27,7 +27,7 @@
 .Op Fl r Ar id
 .Sh DESCRIPTION
 .Nm
-is a terminal coding agent native to
+is a personal terminal coding agent for one human Owner, native to
 .Ox .
 It sends a conversation to a configured language-model provider and lets the
 model inspect and, when permitted, modify the current project directory using
@@ -38,6 +38,13 @@ external-editor execution are separated into processes
 and
 .Xr unveil 2
 restrictions.
+It can use the Owner's Claude Pro or Max subscription through an experimental,
+unofficial pure-C compatibility profile, or a separately billed Anthropic or
+OpenAI-compatible API.
+The subscription profile may stop working or trigger upstream account
+enforcement.
+It is personal CLI use only: it must not be hosted, exposed as an API or
+service, shared with another human, or used for team or customer traffic.
 .
 .Pp
 With no options,
@@ -281,7 +288,7 @@ This command neither modifies nor journals the convers
 End the session.
 .It Cm / Ns Ar skill Op Ar text
 Invoke a loaded skill.
-The skill body becomes the user message and any trailing text is appended as
+The skill body becomes the Owner message and any trailing text is appended as
 a second paragraph.
 .El
 .
@@ -441,7 +448,7 @@ not sent to the subagent.
 .Pp
 The optional
 .Cm model
-argument is an exact member of the operator-configured palette shown in the
+argument is an exact member of the Owner-configured palette shown in the
 tool schema.
 With only the implicit provider its entries are bare model identifiers.
 When named providers exist, an implicit-provider entry is
@@ -457,7 +464,7 @@ is not parsed as a separator.
 The palette contains the configured model of each provider and the configured
 .Cm subagent_model ,
 with duplicate routes removed.
-An implicit provider without an effective API key is omitted.
+An implicit provider without an effective credential is omitted.
 Omitting
 .Cm model
 uses
@@ -506,7 +513,23 @@ call and returned report are ordinary transactional to
 are replayed on resume.
 .
 .Sh PROVIDERS
-Anthropic Messages and OpenAI-compatible chat-completions providers use
+The experimental Claude subscription Provider sends the Owner's long-lived
+inference token directly to a fixed Anthropic TLS endpoint using a versioned,
+observed Claude Code-compatible request profile implemented in C.
+It requires no Node.js, Python, Claude Code executable, or Agent SDK runtime.
+The profile supplies compatibility identity, attribution, beta, cache, and
+Tool-name transformations; fugu still validates and executes every requested
+Tool under its normal kernel confinement.
+This is not an Anthropic-supported third-party API contract.
+It may stop working without notice or lead to account limitation or suspension.
+fugu does not probe alternate client identities, evade quotas or safety
+controls, or silently fall back to a metered API Provider.
+See
+.Xr fugu.conf 5
+for the inherent one-Owner deployment boundary and token setup.
+.
+.Pp
+Direct Anthropic Messages and OpenAI-compatible chat-completions providers use
 streaming HTTPS and may invoke tools.
 Anthropic requests use ephemeral prompt-cache breakpoints; OpenAI-compatible
 servers manage their own caches.
@@ -519,7 +542,7 @@ A
 .Li Retry-After
 delta is clamped to 1 through 60 seconds.
 Other HTTP 4xx responses and failures after streaming begins end the turn.
-Provider and TLS error details are shown to the user without exposing
+Provider and TLS error details are shown to the Owner without exposing
 credentials.
 .
 .Sh SESSIONS
@@ -633,6 +656,18 @@ source.
 .An Isaac Aq Mt isaac@itm.works
 .
 .Sh CAVEATS
+Claude subscription mode assumes one Owner and a one-person installation; the
+Owner is not separately configured.
+It is not an authentication mechanism between local accounts: do not install a
+credential-bearing subscription configuration on a multi-user host or route
+another person's, a team's, or a service's work through the Owner's Pro or Max
+quota.
+Use a direct API provider for shared, hosted, service, or production-facing
+operation.
+The compatibility profile is undocumented and unofficial; using it carries
+breakage, terms, and account-enforcement risk for the Owner.
+.
+.Pp
 Within the starting project directory, the model can read, modify, or delete
 anything not covered by
 .Cm protect .
@@ -687,10 +722,14 @@ The model and all model-selected file names, commands,
 pages, and provider streams are untrusted.
 The tool executor is confined to the starting project directory and selected
 base-system executable and library paths.
-It has no network capability unless the operator explicitly enables network
+It has no network capability unless the Owner explicitly enables network
 access for shell subprocesses.
 The web worker cannot write files or execute programs, and model credentials
 are confined to the API worker.
+For direct API and subscription routes alike, that worker cannot execute
+programs, write files, or reopen files after loading its trust store.
+The OAuth token never reaches a model-selected Tool, subagent Tool role, front
+end, journal, or log.
 .
 .Pp
 Each subagent runs in a fresh tool role whose own steady pledge omits write,
blob - 4f69669b6fb8484f887f3b214d145c03139eeecd
blob + 6e230ba39625996c74c90c4214ffc8af98c7f310
--- src/fugu/fugu.conf.5
+++ src/fugu/fugu.conf.5
@@ -24,7 +24,8 @@ The
 .Nm
 file configures
 .Xr fugu 1
-providers, credentials, tools, interface behavior, and per-user policy.
+providers, credentials, tools, interface behavior, and policy for its one
+human Owner.
 The default path is
 .Pa /etc/fugu.conf .
 The installed
@@ -92,6 +93,51 @@ A privileged execution ignores it and every other
 .Ev FUGU_*
 override.
 .
+.Sh CLAUDE SUBSCRIPTION USE
+The
+.Cm claude
+Provider uses one human's Claude Pro or Max subscription credential through an
+experimental, unofficial Claude Code-compatible request profile implemented
+entirely in C.
+That human is called the
+.Em Owner .
+It is personal CLI use only.
+Do not host it, expose it as an API or service, share it with another human,
+or route team or customer traffic through the Owner's Claude Pro or Max
+subscription.
+Use a direct API provider for those cases.
+The compatibility profile is not an Anthropic-supported third-party API
+contract; it may stop working without notice or trigger account limitation or
+suspension.
+.
+.Pp
+The one human Owner is inherent in fugu's personal installation and invocation
+pattern; there is no separate Owner identity or account-routing directive.
+Generate a long-lived inference token as that person with:
+.Bd -literal -offset indent
+$ claude setup-token
+.Ed
+Paste the token's single output line directly into the protected
+.Pa /etc/fugu.conf
+using
+.Cm oauth_token .
+The command is used only to provision the token; fugu neither installs nor
+executes Claude Code.
+The token is passed only to the model-credential custodian; it is never passed
+to a fugu Tool, subprocess, front end, log, or journal.
+.
+.Pp
+The Provider endpoint is fixed to Anthropic over TLS and cannot be overridden.
+The versioned profile supplies observed identity, billing-attribution, beta,
+cache, and Tool-name transformations before the existing Messages transport
+and hostile SSE parser run.
+fugu remains responsible for validating and executing every Tool call under
+its normal confinement.
+There is no Node.js, Python, Claude Code, or Agent SDK runtime dependency, no
+interactive OAuth flow, and no automatic token refresh.
+fugu does not try alternate fingerprints or silently fall back to a metered API
+Provider when compatibility fails.
+.
 .Sh EVALUATION
 Configuration values are resolved in this order:
 .Bl -enum -compact
@@ -157,6 +203,8 @@ and
 .Pa api.openai.com
 for
 .Cm openai .
+This directive is rejected when the implicit Provider is
+.Cm claude .
 .It Cm api_path Ar path
 Set the implicit provider's endpoint path.
 The default is
@@ -167,10 +215,14 @@ and
 .Pa /v1/chat/completions
 for
 .Cm openai .
+This directive is rejected when the implicit Provider is
+.Cm claude .
 .It Cm api_port Ar port
 Set the implicit provider's TLS service or numeric port.
 The default is 443.
 A numeric value must be between 1 and 65535.
+This directive is rejected when the implicit Provider is
+.Cm claude .
 Plaintext endpoints are not supported.
 Endpoint hosts and paths are limited to 255 bytes and service names to 15
 bytes.
@@ -254,9 +306,14 @@ Protecting a directory covers its subtree.
 Names may remain visible in directory listings.
 .It Cm provider Ar type
 Set the implicit provider wire protocol to
+.Cm claude ,
 .Cm anthropic
 or
 .Cm openai .
+The
+.Cm claude
+value selects the experimental personal subscription compatibility profile;
+the other values select supported direct HTTP APIs.
 The default is
 .Cm anthropic .
 .It Cm search_provider Ar name
@@ -282,7 +339,8 @@ argument.
 The value must identify a configured named provider block, or be
 .Dq @default
 to select the implicit provider.
-The latter is rejected unless the implicit provider has an effective API key.
+The latter is rejected unless the implicit provider has an effective
+credential.
 A named block literally named
 .Dq default
 is selected by
@@ -333,6 +391,10 @@ is present.
 .It Cm anthropic_key_file Ar path
 Read the legacy Anthropic credential from the first line of
 .Ar path .
+.It Cm oauth_token Ar token
+Set the implicit Claude subscription credential inline.
+This directive is valid only with
+.Cm provider "claude" .
 .It Cm kagi_token Ar token
 Set the Kagi search credential inline.
 .It Cm kagi_token_file Ar path
@@ -355,10 +417,19 @@ provider "work" {
 .Ed
 Provider names must be 1 through 63 bytes, contain only letters, digits,
 hyphens, and underscores, and be unique.
-Each block requires a key.
+Each block requires an effective credential:
+.Cm oauth_token
+for
+.Cm claude ,
+or
+.Cm api_key
+for a direct API.
 When no flat implicit provider credential is configured, the first named
 block is active at startup.
 Selecting a model also selects its provider for the remainder of the session.
+Because OAuth model discovery is not assumed, each
+.Cm claude
+Provider contributes only its explicitly configured model to the picker.
 .
 .Pp
 Configured provider models form the
@@ -375,13 +446,15 @@ uses
 .Li @default:model .
 These are exact enum values, not strings split at the colon; model identifiers
 may therefore contain colons.
-An implicit provider without an effective API key is omitted from the palette.
+An implicit provider without an effective credential is omitted from the
+palette.
 .
 .Pp
 The following directives are accepted inside a named provider block:
 .Bl -tag -width Ds
-.It Cm type Cm anthropic | openai
-Select the wire protocol.
+.It Cm type Cm claude | anthropic | openai
+Select the experimental personal Claude subscription compatibility profile or
+a direct wire protocol.
 The default for a block is
 .Cm anthropic .
 .It Cm model Ar model
@@ -389,16 +462,26 @@ Set the block's default model identifier.
 .It Cm api_host Ar host
 Set the endpoint host, with the same per-type defaults as the implicit
 provider.
+This directive is rejected for
+.Cm claude .
 .It Cm api_port Ar port
 Set the TLS service or port; the default is 443.
+This directive is rejected for
+.Cm claude .
 .It Cm api_path Ar path
 Set the request path, with the same per-type defaults as the implicit
 provider.
+This directive is rejected for
+.Cm claude .
 .It Cm api_key Ar key
 Set the block's credential inline.
 .It Cm api_key_file Ar path
 Read the block's credential from the first line of
 .Ar path .
+.It Cm oauth_token Ar token
+Set a Claude subscription credential inline.
+This directive is valid only in a block whose type is
+.Cm claude .
 .El
 .
 .Sh FILES
@@ -410,8 +493,17 @@ Fully commented package example.
 .El
 .
 .Sh EXAMPLES
-A minimal Anthropic configuration:
+A minimal personal Claude Max configuration:
 .Bd -literal -offset indent
+provider "claude"
+model "claude-sonnet-4-6"
+oauth_token "sk-ant-oat01-..."
+.Ed
+.
+.Pp
+A direct Anthropic API configuration, which is not charged to a Claude
+subscription:
+.Bd -literal -offset indent
 api_key_file "/etc/fugu-anthropic.key"
 kagi_token_file "/etc/fugu-kagi.key"
 .Ed
@@ -428,12 +520,12 @@ http_allow "api.github.com"
 .Ed
 .
 .Pp
-Several providers with first-match per-user policy:
+Several providers for the same Owner:
 .Bd -literal -offset indent
 provider "personal" {
-	type "anthropic"
+	type "claude"
 	model "claude-sonnet-4-6"
-	api_key_file "/etc/fugu-personal.key"
+	oauth_token "sk-ant-oat01-..."
 }
 provider "work" {
 	type "openai"
@@ -447,10 +539,8 @@ max_subagents 4
 subagent_provider "work"
 subagent_model "gpt-5"
 
-match group "contractors" {
+match user "alice" {
 	allow_write no
-	web_search no
-	max_subagents 0
 }
 .Ed
 .
@@ -459,6 +549,23 @@ match group "contractors" {
 .Xr glob 3
 .
 .Sh SECURITY CONSIDERATIONS
+The Owner relationship is inherent in the personal release pattern, not a
+configuration label or local authentication boundary.
+A Claude Pro or Max subscription credential must serve only its human Owner.
+Do not install a credential-bearing subscription configuration on a multi-user
+host: fugu does not distinguish local accounts for subscription authorization.
+Subscription secrets are forbidden in
+.Cm match
+blocks, but that parser rule is not a substitute for the one-person deployment
+boundary.
+The compatibility route is unofficial and undocumented and may stop working
+or cause Anthropic to limit or suspend the Owner's account.
+It must not be used to evade subscription quotas, safety controls, or account
+enforcement.
+Direct API Providers are the supported route when a stable authentication
+contract is required.
+.
+.Pp
 The configuration controls the kernel authority granted to model-selected
 actions.
 .Cm allow_subprocess_net yes
blob - 112ac28d96d6c8913ff0e4ec1de5f3d5857bd97e
blob + 3fc18bfb726bbd4a2617b233e5f17e4adf7ebea7
--- src/fugu/main.c
+++ src/fugu/main.c
@@ -281,7 +281,7 @@ main(int argc, char *argv[])
 	}
 	if (lflag) {
 		/*
-		 * -l reads only the user's own session journals, never the
+		 * -l reads only the Owner's own session journals, never the
 		 * _fugu-group config, but it parses those journals (untrusted
 		 * bytes: project context, model output, I13) -- so shed the
 		 * setgid group first, as the interactive/print paths do before
blob - 5f810745077e13d960888de5c7afbfdce0812cad
blob + 21ec4235f66e401447251a9e19e7c6e0509a3ee9
--- src/fugu/parse.y
+++ src/fugu/parse.y
@@ -133,7 +133,8 @@ typedef struct {
 %token	ALLOWSUBPROCESSNET ALLOWWRITE ANTHROPICKEY ANTHROPICKEYFILE APIHOST
 %token	APIKEY APIKEYFILE APIPATH APIPORT ASCII CONTEXTLIMIT ERROR GROUP
 %token	HTTPALLOW HTTPBLOCK INCLUDE KAGITOKEN KAGITOKENFILE MATCH MAXBGJOBS
-%token	MAXSUBAGENTS MAXTOKENS MODEL NO NOTIFY PROJECTCONTEXT PROTECT
+%token	MAXSUBAGENTS MAXTOKENS MODEL NO NOTIFY OAUTHTOKEN
+%token	PROJECTCONTEXT PROTECT
 %token	PROVIDER SEARCHPROVIDER SUBAGENTMODEL SUBAGENTPROVIDER SYSTEM TYPE
 %token	USER WEBSEARCH YES
 %token	<v.string>	STRING
@@ -339,6 +340,14 @@ main		: MODEL STRING			{
 			if (set_secret(&conf->anthropic_key, key) == -1)
 				YYERROR;
 		}
+		| OAUTHTOKEN STRING		{
+			if (!secret_ok()) {
+				free_secret($2);
+				YYERROR;
+			}
+			if (set_secret(&conf->oauth_token, $2) == -1)
+				YYERROR;
+		}
 		| KAGITOKEN STRING		{
 			if (!secret_ok()) {
 				free_secret($2);
@@ -410,13 +419,24 @@ matchoptsl	: main
 		;
 
 providerblk	: providerstart '{' optnl provopts_l '}'	{
-			if (prov->api_key == NULL) {
-				yyerror("provider %s: no api_key", prov->name);
+			if ((prov->type == PROVIDER_CLAUDE &&
+			    prov->oauth_token == NULL) ||
+			    (prov->type != PROVIDER_CLAUDE &&
+			    prov->api_key == NULL)) {
+				yyerror("provider %s: no %s", prov->name,
+				    prov->type == PROVIDER_CLAUDE ?
+				    "oauth_token" : "api_key");
 				free(prov->name);
 				free(prov->model);
 				free(prov->api_host);
 				free(prov->api_port);
 				free(prov->api_path);
+				if (prov->api_key != NULL)
+					freezero(prov->api_key,
+					    strlen(prov->api_key));
+				if (prov->oauth_token != NULL)
+					freezero(prov->oauth_token,
+					    strlen(prov->oauth_token));
 				free(prov);
 				prov = NULL;
 				YYERROR;
@@ -511,6 +531,10 @@ provopt		: TYPE STRING			{
 			if (set_secret(&prov->api_key, key) == -1)
 				YYERROR;
 		}
+		| OAUTHTOKEN STRING		{
+			if (set_secret(&prov->oauth_token, $2) == -1)
+				YYERROR;
+		}
 		;
 
 optnl		: '\n' optnl
@@ -578,6 +602,7 @@ lookup(char *s)
 		{ "model",			MODEL },
 		{ "no",				NO },
 		{ "notify",			NOTIFY },
+		{ "oauth_token",		OAUTHTOKEN },
 		{ "project_context",		PROJECTCONTEXT },
 		{ "protect",			PROTECT },
 		{ "provider",			PROVIDER },
@@ -994,21 +1019,71 @@ parse_config(const char *filename, struct fugu_conf *x
 		free(prov->api_path);
 		if (prov->api_key != NULL)
 			freezero(prov->api_key, strlen(prov->api_key));
+		if (prov->oauth_token != NULL)
+			freezero(prov->oauth_token, strlen(prov->oauth_token));
 		free(prov);
 		prov = NULL;
 	}
 	if (errors == 0) {
 		struct fugu_provider	*p;
 
-		if (provider_wire_ok("default provider", conf_api_host(conf),
+		if (conf->provider_type != PROVIDER_CLAUDE &&
+		    provider_wire_ok("default provider", conf_api_host(conf),
 		    conf_api_port(conf), conf_api_path(conf)) == -1)
 			errors++;
 		TAILQ_FOREACH(p, &conf->providers, entry)
-			if (provider_wire_ok(p->name, conf_prov_host(p),
+			if (p->type != PROVIDER_CLAUDE &&
+			    provider_wire_ok(p->name, conf_prov_host(p),
 			    conf_prov_port(p), conf_prov_path(p)) == -1)
 				errors++;
 	}
 
+	if (errors == 0) {
+		struct fugu_provider	*p;
+
+		if (conf->provider_type == PROVIDER_CLAUDE &&
+		    (conf->api_host != NULL || conf->api_port != NULL ||
+		    conf->api_path != NULL)) {
+			log_warnx("Claude subscription endpoint cannot be overridden");
+			errors++;
+		}
+		if (conf->provider_type == PROVIDER_CLAUDE &&
+		    (conf->api_key != NULL ||
+		    conf->anthropic_key != NULL)) {
+			log_warnx("Claude subscription provider cannot use an API key");
+			errors++;
+		}
+		if (conf->provider_type != PROVIDER_CLAUDE &&
+		    conf->oauth_token != NULL) {
+			log_warnx("oauth_token requires provider claude");
+			errors++;
+		}
+		TAILQ_FOREACH(p, &conf->providers, entry) {
+			if (p->type == PROVIDER_CLAUDE) {
+				if (p->api_host != NULL || p->api_port != NULL ||
+				    p->api_path != NULL) {
+					log_warnx("provider %s: Claude subscription "
+					    "endpoint cannot be overridden", p->name);
+					errors++;
+				}
+				if (p->api_key != NULL) {
+					log_warnx("provider %s: Claude subscription "
+					    "cannot use an API key", p->name);
+					errors++;
+				}
+			} else if (p->oauth_token != NULL) {
+				log_warnx("provider %s: oauth_token requires type "
+				    "claude", p->name);
+				errors++;
+			}
+		}
+		if (conf->provider_type == PROVIDER_CLAUDE &&
+		    conf->oauth_token == NULL && TAILQ_EMPTY(&conf->providers)) {
+			log_warnx("Claude subscription provider requires oauth_token");
+			errors++;
+		}
+	}
+
 	if (errors == 0 && conf->subagent_provider != NULL) {
 		struct fugu_provider	*p;
 
@@ -1246,6 +1321,8 @@ done:
 static int
 parse_provider_type(const char *s)
 {
+	if (strcmp(s, "claude") == 0)
+		return (PROVIDER_CLAUDE);
 	if (strcmp(s, "anthropic") == 0)
 		return (PROVIDER_ANTHROPIC);
 	if (strcmp(s, "openai") == 0)
blob - 37523910880611bd05f9b3059637d3487170f0a5
blob + 33b066010d4001607e945821266929cb6d00ae58
--- src/fugu/proto.h
+++ src/fugu/proto.h
@@ -59,6 +59,7 @@
  * hello_prov (behavior.md section 8). */
 #define PROVIDER_ANTHROPIC	0
 #define PROVIDER_OPENAI		1
+#define PROVIDER_CLAUDE		2
 
 /* Credentials cross one HELLO frame and later become HTTP headers. */
 #define FUGU_SECRET_MAX	(4 * 1024)
@@ -379,6 +380,7 @@ struct hello_tool {
  * payload retains the active lead provider for backward-compatible tests. */
 struct api_request_end {
 	int	provider_slot;		/* 0: implicit default; 1..: named block */
+	char	model[FUGU_MODEL_ID_MAX + 1];
 };
 
 /*
@@ -404,6 +406,7 @@ struct hello_api {
 	char	host[FUGU_API_HOST_MAX + 1];
 	char	port[FUGU_API_PORT_MAX + 1];
 	char	path[FUGU_API_PATH_MAX + 1];
+	char	model[FUGU_MODEL_ID_MAX + 1];
 	/* model key follows, not NUL-terminated */
 };
 
@@ -482,6 +485,7 @@ struct hello_prov {
 	char	host[FUGU_API_HOST_MAX + 1];
 	char	port[FUGU_API_PORT_MAX + 1];
 	char	path[FUGU_API_PATH_MAX + 1];
+	char	model[FUGU_MODEL_ID_MAX + 1];
 };
 
 /* One model id out of a provider's listing (MODEL, UI_MODEL); the
blob - eeebe2832165b8c69a49bb65589449d50c7628b8
blob + a5152acb7c0cd80ec9b6c05e015f024bee93b536
--- src/fugu/skills.h
+++ src/fugu/skills.h
@@ -24,7 +24,7 @@
 /*
  * User-installed skills (behavior.md section 4): prompt templates
  * under ~/.fugu/skills/<name>/SKILL.md, loaded once at startup.  A
- * skill's slash name fires its body as the user's message; unless
+ * skill's slash name fires its body as the Owner's message; unless
  * disabled, the skill is also offered to the model as the `skill`
  * tool (section 5), whose result is the same body.
  */
blob - 574903da29d581811339be12951be575acc95b51
blob + 9e087a3935dde6b66f96ce719551094eec91caae
--- src/fugu/sysprompt.c
+++ src/fugu/sysprompt.c
@@ -23,12 +23,12 @@
  */
 const char	fugu_system_prompt[] =
 "You are fugu, a coding assistant running in a terminal on an "
-"OpenBSD/Unix system. You work inside the user's project directory "
+"OpenBSD/Unix system. You work inside the Owner's project directory "
 "through local tools under kernel-enforced confinement.\n"
 "\n"
 "Answer style: be terse and skip preamble. Get to the point; no "
 "\"Sure!\" or \"Here is\". Use Markdown, and refer to code as "
-"file:line so the user can jump to it. Explain only what was asked.\n"
+"file:line so the Owner can jump to it. Explain only what was asked.\n"
 "\n"
 "Task discipline: do exactly what is asked, nothing more. Match the "
 "surrounding code's style and conventions. Do not add comments, "
blob - 2face3b426da2982884644785adde934639bdd41
blob + 0198d2d5dc4e13214ca90ecb0c489c1aeb4c437d
--- src/fugu/sysprompt.h
+++ src/fugu/sysprompt.h
@@ -17,7 +17,7 @@
 #ifndef SYSPROMPT_H
 #define SYSPROMPT_H
 
-/* The built-in system prompt, sent when the operator's `system` key
+/* The built-in system prompt, sent when the Owner's `system` key
  * is unset (behavior.md section 8). */
 extern const char	fugu_system_prompt[];
 
blob - 75b51537e80dde5c3cc10d8066f30c1004eb53c0
blob + 46e21744e61fc23203a9c1685b08b0544ac70cba
--- src/fugu/tooldefs.c
+++ src/fugu/tooldefs.c
@@ -124,7 +124,7 @@ static const struct tool_def all_web_fetch = {
 
 static const struct tool_def all_http_request = {
 	"http_request",
-	"Send an HTTPS request only to an operator-allowed destination. "
+	"Send an HTTPS request only to an Owner-allowed destination. "
 	"No fugu-held credential is attached. Non-2xx responses are returned "
 	"normally; redirects are not followed and private destinations are "
 	"refused.",
blob - 8f27e41feb168bb31de52e29c0a3790e4662b198
blob + 2fefbd81e0b31e9b08c2adc9f39d0021c4b4ad35
--- src/fugu-api/Makefile
+++ src/fugu-api/Makefile
@@ -1,5 +1,5 @@
 PROG=	fugu-api
-SRCS=	main.c worker.c imsgev.c \
+SRCS=	main.c worker.c imsgev.c claude_sub_profile.c claude_sub_wire.c \
 	http.c anthropic_stream.c openai_stream.c sse.c json.c buf.c \
 	model_window.c log.c xmalloc.c
 BINDIR=	${LIBEXECDIR}/fugu
blob - 3b8b8723d9c7524969a9ff6212020e3950c0a91f
blob + bf01efcf15f2cd29b2d1068e0e201b2362451e6a
--- src/fugu-api/main.c
+++ src/fugu-api/main.c
@@ -54,6 +54,7 @@
 #include "model_window.h"
 #include "http.h"
 #include "anthropic.h"
+#include "claude_sub.h"
 #include "openai.h"
 #include "worker.h"
 #include "proto.h"
@@ -63,7 +64,7 @@
 #define API_ERRBODY_MAX	16384
 #define API_ERRBODY_READ_MS 1000
 #define API_ERRBODY_TOTAL_S 5
-#define API_AUTH_HDR_MAX (FUGU_SECRET_MAX + 128)
+#define API_AUTH_HDR_MAX (FUGU_SECRET_MAX + 4096)
 #define API_REQUEST_MAX 65	/* lead plus one full tool-call wave */
 #define API_NETWORK_MAX 8	/* bounds concurrent hostile stream state */
 #define API_REQUEST_MEMORY_MAX (64 * 1024 * 1024)
@@ -94,6 +95,7 @@ struct prov {
 	char	 host[FUGU_API_HOST_MAX + 1];
 	char	 port[FUGU_API_PORT_MAX + 1];
 	char	 path[FUGU_API_PATH_MAX + 1];
+	char	 model[FUGU_MODEL_ID_MAX + 1];
 	char	*key;			/* secret; NUL-free; NULL unset */
 };
 
@@ -160,6 +162,8 @@ struct api_request {
 	struct worker		*worker;
 	uint32_t		 id;
 	size_t			 slot;
+	char			 model[FUGU_MODEL_ID_MAX + 1];
+	char			 request_id[37];
 	enum request_phase	 phase;
 	struct buf		 body;
 	struct buf		 wire;
@@ -209,6 +213,7 @@ struct api {
 	size_t			 ninflight;
 	size_t			 request_memory;
 	int			 list_pending;
+	char			 session_id[37];
 };
 
 static const char *
@@ -258,6 +263,62 @@ fixed_cstr(const char *s, size_t cap, int empty_ok)
 }
 
 static int
+claude_regress_port(const char **out)
+{
+	const char	*errstr, *port;
+
+	*out = NULL;
+	if (issetugid() || (port = getenv("FUGU_REGRESS_CLAUDE_PORT")) == NULL)
+		return (0);
+	(void)strtonum(port, 1, 65535, &errstr);
+	if (errstr != NULL)
+		return (-1);
+	*out = port;
+	return (1);
+}
+
+static int
+provider_endpoint_ok(int type, const char *host, const char *port,
+    const char *path)
+{
+	const char	*regress_port;
+	int		 rc;
+
+	if (type != PROVIDER_CLAUDE)
+		return (1);
+	if (strcmp(host, CLAUDE_SUB_HOST) == 0 &&
+	    strcmp(port, CLAUDE_SUB_PORT) == 0 &&
+	    strcmp(path, CLAUDE_SUB_PATH) == 0)
+		return (1);
+	rc = claude_regress_port(&regress_port);
+	return (rc == 1 && strcmp(host, "127.0.0.1") == 0 &&
+	    strcmp(port, regress_port) == 0 &&
+	    strcmp(path, CLAUDE_SUB_PATH) == 0);
+}
+
+static void
+uuid_v4(char out[37])
+{
+	u_char b[16];
+
+	arc4random_buf(b, sizeof(b));
+	b[6] = (b[6] & 0x0f) | 0x40;
+	b[8] = (b[8] & 0x3f) | 0x80;
+	(void)snprintf(out, 37,
+	    "%02x%02x%02x%02x-%02x%02x-%02x%02x-%02x%02x-"
+	    "%02x%02x%02x%02x%02x%02x",
+	    (unsigned int)b[0], (unsigned int)b[1],
+	    (unsigned int)b[2], (unsigned int)b[3],
+	    (unsigned int)b[4], (unsigned int)b[5],
+	    (unsigned int)b[6], (unsigned int)b[7],
+	    (unsigned int)b[8], (unsigned int)b[9],
+	    (unsigned int)b[10], (unsigned int)b[11],
+	    (unsigned int)b[12], (unsigned int)b[13],
+	    (unsigned int)b[14], (unsigned int)b[15]);
+	explicit_bzero(b, sizeof(b));
+}
+
+static int
 api_lockdown(struct worker *w, struct imsg *imsg)
 {
 	struct api		*a = w->ctx;
@@ -275,10 +336,13 @@ api_lockdown(struct worker *w, struct imsg *imsg)
 		fatalx("fugu-api: short HELLO");
 	memcpy(&h, p, sizeof(h));
 	if ((h.provider_type != PROVIDER_ANTHROPIC &&
-	    h.provider_type != PROVIDER_OPENAI) ||
+	    h.provider_type != PROVIDER_OPENAI &&
+	    h.provider_type != PROVIDER_CLAUDE) ||
 	    !fixed_cstr(h.host, sizeof(h.host), 0) ||
 	    !fixed_cstr(h.port, sizeof(h.port), 0) ||
-	    !fixed_cstr(h.path, sizeof(h.path), 0) || h.path[0] != '/')
+	    !fixed_cstr(h.path, sizeof(h.path), 0) || h.path[0] != '/' ||
+	    !fixed_cstr(h.model, sizeof(h.model), 0) ||
+	    !provider_endpoint_ok(h.provider_type, h.host, h.port, h.path))
 		fatalx("fugu-api: malformed HELLO endpoint");
 
 	/* slot 0: the implicit default provider */
@@ -290,6 +354,7 @@ api_lockdown(struct worker *w, struct imsg *imsg)
 	strlcpy(v->host, h.host, sizeof(v->host));
 	strlcpy(v->port, h.port, sizeof(v->port));
 	strlcpy(v->path, h.path, sizeof(v->path));
+	strlcpy(v->model, h.model, sizeof(v->model));
 	v->key = copy_key(p + sizeof(h), len - sizeof(h));
 	if (len > sizeof(h))
 		explicit_bzero((u_char *)p + sizeof(h), len - sizeof(h));
@@ -300,7 +365,7 @@ api_lockdown(struct worker *w, struct imsg *imsg)
 	if (tls_config_set_ca_file(a->tlscfg, ca_path()) == -1)
 		fatalx("ca file: %s", tls_config_error(a->tlscfg));
 
-	/* Trust store loaded: shed the ability to open files (I1). */
+	/* Trust store loaded: every Provider uses the no-files/no-exec Custodian. */
 	if (pledge("stdio inet dns", NULL) == -1)
 		fatal("pledge");
 	return (0);
@@ -323,11 +388,14 @@ prov_add(struct api *a, struct imsg *imsg)
 	if (p == NULL || len < sizeof(hp))
 		fatalx("fugu-api: short PROV");
 	memcpy(&hp, p, sizeof(hp));
-	if ((hp.type != PROVIDER_ANTHROPIC && hp.type != PROVIDER_OPENAI) ||
+	if ((hp.type != PROVIDER_ANTHROPIC && hp.type != PROVIDER_OPENAI &&
+	    hp.type != PROVIDER_CLAUDE) ||
 	    !fixed_cstr(hp.name, sizeof(hp.name), 0) ||
 	    !fixed_cstr(hp.host, sizeof(hp.host), 0) ||
 	    !fixed_cstr(hp.port, sizeof(hp.port), 0) ||
-	    !fixed_cstr(hp.path, sizeof(hp.path), 0) || hp.path[0] != '/')
+	    !fixed_cstr(hp.path, sizeof(hp.path), 0) || hp.path[0] != '/' ||
+	    !fixed_cstr(hp.model, sizeof(hp.model), 1) ||
+	    !provider_endpoint_ok(hp.type, hp.host, hp.port, hp.path))
 		fatalx("fugu-api: malformed provider endpoint");
 
 	a->provs = xreallocarray(a->provs, a->nprov + 1, sizeof(*a->provs));
@@ -338,6 +406,7 @@ prov_add(struct api *a, struct imsg *imsg)
 	strlcpy(v->host, hp.host, sizeof(v->host));
 	strlcpy(v->port, hp.port, sizeof(v->port));
 	strlcpy(v->path, hp.path, sizeof(v->path));
+	strlcpy(v->model, hp.model, sizeof(v->model));
 	v->key = copy_key(p + sizeof(hp), len - sizeof(hp));
 	if (len > sizeof(hp))
 		explicit_bzero((u_char *)p + sizeof(hp), len - sizeof(hp));
@@ -733,18 +802,34 @@ on_event(const struct astream_event *ev, void *arg)
 	case ASTREAM_TOOL_BEGIN: {
 		struct buf	 b;
 		struct a_tool_begin hdr;
+		char		 canonical[FUGU_TOOL_NAME_MAX];
+		const char	*tool_name;
 		size_t		 idlen, namelen;
 
-		if (ev->tool_id == NULL || ev->tool_name == NULL ||
+		tool_name = ev->tool_name;
+		if (ev->tool_id == NULL || tool_name == NULL ||
 		    (idlen = strlen(ev->tool_id)) == 0 ||
 		    idlen >= API_TOOL_ID_MAX ||
-		    (namelen = strlen(ev->tool_name)) == 0 ||
-		    namelen >= FUGU_TOOL_NAME_MAX ||
 		    contains_secret(a, ev->tool_id, idlen) ||
-		    contains_secret(a, ev->tool_name, namelen)) {
+		    contains_secret(a, tool_name, strlen(tool_name))) {
 			relay_error(r, "invalid provider tool identifier");
 			break;
 		}
+		if (a->provs[r->slot].type == PROVIDER_CLAUDE) {
+			if (claude_sub_tool_unmap(canonical, sizeof(canonical),
+			    tool_name) == -1) {
+				relay_error(r,
+				    "invalid Claude subscription tool identifier");
+				break;
+			}
+			tool_name = canonical;
+		}
+		namelen = strlen(tool_name);
+		if (namelen == 0 || namelen >= FUGU_TOOL_NAME_MAX ||
+		    contains_secret(a, tool_name, namelen)) {
+			relay_error(r, "invalid provider tool identifier");
+			break;
+		}
 		if (tool_stream_begin(r, ev->index) == NULL) {
 			relay_error(r, "invalid provider tool-call sequence");
 			break;
@@ -756,7 +841,7 @@ on_event(const struct astream_event *ev, void *arg)
 		buf_add(&b, &hdr, sizeof(hdr));
 		buf_addstr(&b, ev->tool_id);
 		buf_addc(&b, '\0');
-		buf_addstr(&b, ev->tool_name);
+		buf_addstr(&b, tool_name);
 		relay(r, FUGU_IMSG_A_TOOL_BEGIN, b.data, b.len);
 		buf_free(&b);
 		break;
@@ -905,10 +990,14 @@ request_retry_after(struct http *h)
 }
 
 static int
-request_auth_headers(const struct prov *v, char *out, size_t outsz)
+request_auth_headers(const struct api_request *r, char *out, size_t outsz)
 {
+	const struct prov *v = &r->api->provs[r->slot];
 	int n;
 
+	if (v->type == PROVIDER_CLAUDE)
+		return (claude_sub_build_headers(out, outsz, v->key, r->model,
+		    r->api->session_id, r->request_id));
 	if (v->type == PROVIDER_OPENAI)
 		n = snprintf(out, outsz,
 		    "Authorization: Bearer %s\r\n"
@@ -1195,24 +1284,30 @@ static void
 request_error_done(struct api_request *r)
 {
 	struct prov *v = &r->api->provs[r->slot];
-	char *reason;
-	size_t reasonsz = API_ERRBODY_MAX + 32;
+	char *raw, *reason;
+	size_t rawsz = API_ERRBODY_MAX + 32;
 	int retryable = r->retryable;
 	int retry_after = r->retry_after;
 
-	reason = xcalloc(reasonsz, 1);
+	raw = xcalloc(rawsz, 1);
 	if ((v->type == PROVIDER_OPENAI ?
-	    openai_error_body(r->errbody.data, r->errbody.len, reason,
-	    reasonsz) :
-	    anthropic_error_body(r->errbody.data, r->errbody.len, reason,
-	    reasonsz)) == -1)
-		(void)snprintf(reason, reasonsz, "HTTP %d from provider",
+	    openai_error_body(r->errbody.data, r->errbody.len, raw,
+	    rawsz) :
+	    anthropic_error_body(r->errbody.data, r->errbody.len, raw,
+	    rawsz)) == -1)
+		(void)snprintf(raw, rawsz, "HTTP %d from provider",
 		    r->http.status);
+	if (v->type == PROVIDER_CLAUDE)
+		xasprintf(&reason, "Claude subscription profile %s: %s",
+		    CLAUDE_SUB_PROFILE_ID, raw);
+	else
+		reason = xstrdup(raw);
 	if (retryable)
 		request_retry(r, reason, retry_after);
 	else
 		request_fail(r, reason);
-	freezero(reason, reasonsz);
+	freezero(reason, strlen(reason));
+	freezero(raw, rawsz);
 }
 
 /* Consume complete HTTP units already buffered.  One means the request was
@@ -1440,7 +1535,7 @@ request_handshake(struct api_request *r)
 		    tls_error(r->http.tls) : "TLS handshake failed", -1);
 		return;
 	}
-	buildrc = request_auth_headers(v, headers, sizeof(headers));
+	buildrc = request_auth_headers(r, headers, sizeof(headers));
 	if (buildrc == 0)
 		buildrc = http_request_head_build(&r->http, &r->wire, "POST", 4,
 		    v->path, strlen(v->path), headers, strlen(headers),
@@ -1543,6 +1638,7 @@ request_start_attempt(struct api_request *r)
 	r->response_bytes = 0;
 	r->tool_args_total = 0;
 	memset(&r->usage, 0, sizeof(r->usage));
+	uuid_v4(r->request_id);
 	http_init(&r->http);
 	r->http_live = 1;
 	r->http.host = xstrdup(v->host);
@@ -1701,10 +1797,37 @@ list_one(struct worker *w, struct api *a, int slot)
 
 	if (v->key == NULL)
 		return;
-	if (request_auth_headers(v, headers, sizeof(headers)) == -1) {
-		explicit_bzero(headers, sizeof(headers));
+	/* OAuth model discovery is not part of the compatibility profile.  A
+	 * Claude Provider contributes only its explicitly configured model. */
+	if (v->type == PROVIDER_CLAUDE) {
+		struct buf	 e;
+		struct model_ent me;
+
+		if (v->model[0] == '\0')
+			return;
+		memset(&me, 0, sizeof(me));
+		me.slot = slot;
+		me.context_window = model_window_effective(0, 0, v->model);
+		buf_init(&e);
+		buf_add(&e, &me, sizeof(me));
+		buf_addstr(&e, v->name);
+		buf_addc(&e, '\0');
+		buf_addstr(&e, v->model);
+		relay_id(w, 0, FUGU_IMSG_MODEL, e.data, e.len);
+		buf_free(&e);
 		return;
 	}
+	{
+		struct api_request fake;
+
+		memset(&fake, 0, sizeof(fake));
+		fake.api = a;
+		fake.slot = (size_t)slot;
+		if (request_auth_headers(&fake, headers, sizeof(headers)) == -1) {
+			explicit_bzero(headers, sizeof(headers));
+			return;
+		}
+	}
 	if (http_connect(&h, v->host, v->port, v->host, a->tlscfg,
 	    LIST_TIMEOUT, NULL, NULL) == -1) {
 		explicit_bzero(headers, sizeof(headers));
@@ -1865,12 +1988,17 @@ api_dispatch(struct worker *w, struct imsg *imsg)
 			if (imsg_get_len(imsg) != sizeof(end) ||
 			    imsg_get_data(imsg, &end, sizeof(end)) == -1 ||
 			    end.provider_slot < 0 ||
-			    (size_t)end.provider_slot >= a->nprov)
+			    (size_t)end.provider_slot >= a->nprov ||
+			    !fixed_cstr(end.model, sizeof(end.model), 0))
 				fatalx("fugu-api: bad request provider slot");
 			r->slot = (size_t)end.provider_slot;
+			strlcpy(r->model, end.model, sizeof(r->model));
 		}
+		if (a->provs[r->slot].type == PROVIDER_CLAUDE &&
+		    r->model[0] == '\0')
+			fatalx("fugu-api: Claude request without model");
 		if (a->provs[r->slot].key == NULL) {
-			relay_error(r, "no API key configured");
+			relay_error(r, "no provider credential configured");
 			request_free(r);
 		} else if (a->ninflight < API_NETWORK_MAX) {
 			a->ninflight++;
@@ -1926,6 +2054,7 @@ main(int argc, char *argv[])
 		fatalx("tls_init");
 
 	a.retry_ms = 1000;
+	uuid_v4(a.session_id);
 	TAILQ_INIT(&a.requests);
 	/* regress compresses the pinned backoff; ignored when setgid */
 	if (!issetugid()) {
@@ -1943,7 +2072,7 @@ main(int argc, char *argv[])
 	w.lockdown = api_lockdown;
 	w.dispatch = api_dispatch;
 
-	/* transitional: rpath only to read the CA bundle */
+	/* Transitional rpath exists only until HELLO loads the CA trust store. */
 	if (pledge("stdio rpath inet dns", NULL) == -1)
 		fatal("pledge");
 
blob - 64cb697e7ff1393b7924d2f6dbc0ff4360892ec7
blob + a1c13f1ff9fa484046e7961b77ce80d83d642d77
--- src/fugu-editor/main.c
+++ src/fugu-editor/main.c
@@ -23,7 +23,7 @@
  * terminal, and returns the result -- which the front end sanitizes
  * and bounds exactly like a paste.  This is the only place
  * $VISUAL/$EDITOR are consulted (I2/I3); the room parses only the
- * user's own seed and the file the user's editor wrote, never model
+ * Owner's own seed and the file the Owner's editor wrote, never model
  * or provider bytes.  No execpromises are set, so the editor itself
  * runs unpledged, as it must.
  */
@@ -53,7 +53,7 @@ static struct buf	 seed;		/* accumulated EDIT_SEED chu
 static volatile sig_atomic_t edit_child; /* running editor pgid */
 static char		 edit_path[64];	/* the live temp file, for SIGTERM cleanup */
 
-/* A coordinator death mid-compose must not orphan the user's editor or
+/* A coordinator death mid-compose must not orphan the Owner's editor or
  * leave the seeded temp file behind ("the temp file never survives"). */
 static void
 on_term(int sig)
@@ -115,7 +115,7 @@ send_result(struct worker *w, int ok, int reason)
 }
 
 /*
- * Run the user's editor on the terminal this room inherited.  The
+ * Run the Owner's editor on the terminal this room inherited.  The
  * editor string may carry flags ("emacsclient -t"): split it on
  * whitespace ourselves rather than through a shell, then append the
  * file path.  execvp does the PATH search; with no execpromises set
@@ -330,11 +330,11 @@ edit_lockdown(struct worker *w, struct imsg *imsg)
 
 	/*
 	 * Steady state: the temp file (rpath wpath cpath), the shared
-	 * terminal (tty), and the fork+exec of the user's editor (proc
+	 * terminal (tty), and the fork+exec of the Owner's editor (proc
 	 * exec).  tmppath is gone from pledge(2), so the file promises
 	 * stay whole-filesystem -- acceptable for a room that holds no
 	 * secrets, runs without the _fugu group, and only ever acts on
-	 * the user's explicit compose keystroke.
+	 * the Owner's explicit compose keystroke.
 	 */
 	if (pledge("stdio rpath wpath cpath tty proc exec", NULL) == -1)
 		fatal("pledge");
blob - 94e8cf9c91dc4d6ff55310800b5d65a0d4580b86
blob + 6f35a281b7f07cccc8ace340c0ed47fdcd8c9a1b
--- src/fugu-tool/Makefile
+++ src/fugu-tool/Makefile
@@ -1,6 +1,7 @@
 PROG=	fugu-tool
 SRCS=	main.c agent.c tools.c job.c supervisor.c worker.c imsgev.c \
-	anthropic_req.c openai_req.c turn_mechanics.c generation.c msg.c json.c \
+	anthropic_req.c claude_sub_req.c claude_sub_profile.c openai_req.c \
+	turn_mechanics.c generation.c msg.c json.c \
 	buf.c diff.c log.c xmalloc.c
 BINDIR=	${LIBEXECDIR}/fugu
 MAN=
blob - e82e6f24aa952e27ca87a4ce61e4bb3a3a48ff58
blob + 1c1397340c9773ece4b600e2a764f3320239b078
--- src/fugu-tool/agent.c
+++ src/fugu-tool/agent.c
@@ -43,6 +43,7 @@
 #include "msg.h"
 #include "turn_mechanics.h"
 #include "anthropic.h"
+#include "claude_sub.h"
 #include "openai.h"
 #include "worker.h"
 #include "proto.h"
@@ -122,7 +123,7 @@ static const struct tool_def agent_web_fetch = {
 };
 
 static const struct tool_def agent_http_request = {
-	"http_request", "Send an HTTPS request to an operator-allowed endpoint.",
+	"http_request", "Send an HTTPS request to an Owner-allowed endpoint.",
 	OBJ("{\"url\":{\"type\":\"string\"},"
 	    "\"method\":{\"type\":\"string\"},"
 	    "\"headers\":{\"type\":\"object\","
@@ -188,7 +189,8 @@ agent_boot_start(struct agent_boot *b, const void *dat
 	ll = strnlen(label, left);
 	if (ll > FUGU_AGENT_LABEL_MAX || ll == left || ll + 1 != left ||
 	    (b->start.provider_type != PROVIDER_ANTHROPIC &&
-	    b->start.provider_type != PROVIDER_OPENAI) ||
+	    b->start.provider_type != PROVIDER_OPENAI &&
+	    b->start.provider_type != PROVIDER_CLAUDE) ||
 	    b->start.provider_slot < 0 || b->start.max_tokens < 1 ||
 	    (b->start.web_search != 0 && b->start.web_search != 1) ||
 	    (b->start.http_request != 0 && b->start.http_request != 1))
@@ -311,6 +313,21 @@ agent_build_request(struct agent_state *a, struct buf 
 		req.ntools = ndefs;
 		req.conv = request->projection.messages;
 		openai_build_request(body, &req);
+	} else if (a->boot->start.provider_type == PROVIDER_CLAUDE) {
+		struct claude_sub_req req;
+
+		memset(&req, 0, sizeof(req));
+		req.model = a->boot->model;
+		req.max_tokens = a->boot->start.max_tokens;
+		req.system = (const char *)a->boot->system.data;
+		req.systemlen = a->boot->system.len;
+		req.tools = defs;
+		req.ntools = ndefs;
+		req.conv = request->projection.messages;
+		req.cache = 1;
+		req.cache_tail_msg = request->projection.cache_tail_msg;
+		if (claude_sub_build_request(body, &req) == -1)
+			fatalx("cannot build Claude subscription request");
 	} else {
 		struct anthropic_req req;