Skip to content

Project Binding

Project Binding lets a supported Access Client keep a local repository synchronized with a remote Caplets runtime. Required Caplets stay hidden unless the session supplies a ready binding context; Caplets never silently runs them in the server startup directory.

A Caplet opts into this requirement explicitly:

{
"cliTools": {
"repo-checks": {
"name": "Repository checks",
"description": "Run approved checks in the attached project.",
"projectBinding": { "required": true },
"actions": {
"test": { "command": "pnpm", "args": ["test"] }
}
}
}
}

Managed Project Binding sessions are available to native generic Current Host and loopback daemon sessions that provide a project root and whose host advertises the capability. The binding session establishes or renews a lease and waits for managed project sync before required Caplets are exposed.

The ordinary caplets attach <url> command is a thin stdio MCP adapter; it does not itself run managed project synchronization. Its --once mode is the CLI preflight for a remote binding endpoint:

Terminal window
cd /path/to/project
caplets remote login https://caplets.example.com
caplets attach https://caplets.example.com --once

A stacked runtime also attempts upstream Project Binding when a client supplies project context and the upstream supports it. If the upstream binding path is unavailable, local project Caplets and non-project upstream Caplets remain available; required upstream project Caplets stay unavailable with a diagnostic.

Choose the client that owns the surrounding workflow:

Use Choose it when
caplets attach An MCP client should launch a thin stdio Adapter and let the CLI use a generic Remote Profile and CLI-managed login state.
@caplets/sdk/project-binding An application already owns Current Host selection, credentials, project identity, and lifecycle UI and needs to embed only the coordinator.

The SDK coordinator is not a programmatic form of Remote Login. It does not discover the Current Host or WebSocket endpoint, persist or refresh credentials, or poll a pending Remote Login. For those managed behaviors, use the CLI flow described in Remote Attach. SDK callers supply an isolated client configured with the Current Host Origin, the exact Project Binding WebSocket URL, and a project fingerprint.

The package root supplies the generated public HTTP client. The browser-safe coordinator is a separate export:

import { createClient } from "@caplets/sdk";
import { runProjectBindingSession } from "@caplets/sdk/project-binding";
const client = createClient({
baseUrl: "https://caplets.example.com",
auth: async () => getAccessToken(),
});
const result = await runProjectBindingSession({
client,
webSocketUrl: "wss://caplets.example.com/api/v1/attach/project-bindings/connect",
projectRoot: "/workspace/repository",
projectFingerprint: fingerprintFromYourApplication,
onEvent(event) {
renderBindingStatus(event);
},
});
if (result.error) {
renderBindingFailure(result.error);
}

createClient creates an isolated Fetch client for the explicit Current Host Origin. Its auth value is optional and can be either a caller-owned static token or an async provider. A Current Host Origin has only scheme, host, and optional port; the SDK rejects credentials, non-root paths, queries, and fragments before network I/O. The example’s credential functions and fingerprint value belong to the application; the SDK does not acquire or store them.

The coordinator has no Node built-ins and can run in a modern browser or Node 22. A Node process that has a filesystem path can use the separate marker-aware helper:

import { fingerprintProjectRoot } from "@caplets/sdk/project-binding/node";
const projectRoot = "/workspace/repository";
const projectFingerprint = fingerprintProjectRoot(projectRoot);

fingerprintProjectRoot hashes the resolved path and recognized project markers. It is not exported from the browser-safe coordinator. Browser callers must calculate or obtain a stable fingerprint themselves and pass it to runProjectBindingSession.

webSocketUrl must be the explicit ws: or wss: endpoint /api/v1/attach/project-bindings/connect. The coordinator derives only its canonical sibling sessions, {bindingId}/heartbeat, and {bindingId}/session HTTP URLs. It does not discover the Current Host Origin or WebSocket URL, and it does not preserve or infer a deployment prefix.

Every socket offers caplets.project-binding.v1 as its first WebSocket subprotocol, and the server always negotiates that exact value. When the isolated client’s auth provider returns a bearer token, the coordinator offers caplets.bearer.<base64url-utf8-token> as a second subprotocol. It resolves async auth again for each HTTP request and for the single reconnect.

Bearer credentials are never placed in the WebSocket URL, session events, SDK error messages, or logs. Do not add tokens to query parameters, application logs, agent configuration, or a persisted fingerprint. The socket URL query carries only the binding ID, session ID, and project fingerprint.

onEvent receives a discriminated union:

Event Meaning
state The server reported a binding and sync-state transition.
ready The binding is usable and includes the binding/session IDs, root, fingerprint, public WebSocket URL, and sync state.
reconnecting An unexpected socket close or error triggered the one allowed reconnect (attempt: 1).
heartbeat The dual heartbeat completed and reports the current server-returned binding and sync state.
ended The terminal reason is known and guarded cleanup has been attempted. This is the final event.

The terminal promise does not resolve at ready; it settles after ended. By default it returns typed fields as either { data, error: undefined } or { data: undefined, error: ProjectBindingSessionError }. Set throwOnError: true to receive ProjectBindingSessionData directly and reject with ProjectBindingSessionError on failure. Abort is a typed failure after cleanup, not a successful end. Error kinds distinguish http, protocol, socket, callback, aborted, and cleanup failures without exposing credentials.

The timing and retry behavior is fixed protocol policy rather than application configuration:

  • send both a WebSocket heartbeat and its sibling HTTP heartbeat immediately after the socket opens, then every 15 seconds;
  • reconnect once after an unexpected close or socket error, then fail if that connection also closes unexpectedly; and
  • finalize exactly once, even when abort, heartbeat, callback, and socket failures race. A live socket receives an end message and gets up to one second for acknowledgement; otherwise the coordinator falls back to the sibling HTTP delete. The initiating failure remains primary and a cleanup failure is attached as secondary detail.

There are intentionally no heartbeat, retry, or finalization tuning options. The coordinator maintains the connection and reports state, but Caplets core remains authoritative for lease ownership and expiry, readiness, synchronization policy, and the bound project copy.

Use the finite smoke path with an explicit remote selector before editing agent configuration:

Terminal window
caplets attach https://caplets.example.com --once

This builds the local sync manifest, enforces preflight size policy, probes the selected Project Binding WebSocket endpoint, and exits. It does not start a sync session and is not a generic MCP health check.

caplets doctor and caplets doctor --json report the resolved project root, fingerprint, bound project path, remote auth mode, WebSocket URL, and recovery command. The standalone CLI does not receive a live sync snapshot, so its sync fields remain idle with default Mutagen diagnostics.

Caplets builds a manifest before synchronization. It applies:

  1. a hard denylist for VCS internals, dependency/build caches, common secret files, private keys, archives, and local environment files;
  2. supported rules from the project’s .gitignore and .git/info/exclude; and
  3. supported rules from optional .capletsignore.

The current matcher supports exact file or directory names, trailing-slash directory rules, leading *. suffix rules, leading-slash root anchoring, and ! negation. Other glob syntax such as **, embedded *, ?, and character classes is treated literally; do not rely on it to protect a sync.

Safe environment templates named .env.example, .env.sample, or .env.template bypass the hard denylist and .capletsignore, but an explicit .gitignore rule still excludes them. Keep real credentials out of templates, and use .gitignore or rename the file when one must not sync. Symbolic links are always skipped.

Caplets creates .caplets/.gitignore with the contents needed to keep binding-local state out of the repository. It does not use that directory as a source of runtime secrets.

The CLI sync preflight enforces the Current Host’s total-project and single-file limits after exclusions. A sync_size_limit_exceeded failure requires excluding unnecessary content or following the host’s stated policy; remote synchronization is never treated as unbounded.

  • Project-bound working directories are resolved under the synchronized root. A configured cwd that escapes through .. or a symbolic link is rejected.
  • Binding leases and readiness metadata are Authoritative Host State. Bound project bytes remain on the owning Host Node.
  • If an owning node is lost, required Caplets are quarantined until the client rebinds to a healthy node and fully resynchronizes. PostgreSQL does not reconstruct bound project bytes.
  • A missing binding context hides only Caplets that declare projectBinding.required; unrelated local and upstream Caplets remain usable.
Diagnostic Meaning Repair
remote_credentials_required The selected Current Host has no valid generic Remote Profile. Run caplets remote login <current-host-origin>.
websocket_upgrade_required A proxy did not pass the WebSocket upgrade. Forward WebSocket upgrades at the canonical origin path.
sync_size_limit_exceeded The post-exclusion project is too large. Add a supported ignore rule or follow the host policy.
lease_conflict or lease_expired Another owner holds the binding or the client stopped heartbeating. End the stale session or reconnect and fully resynchronize.

Start with caplets doctor --json; its Project Binding section carries resolved selection and recovery fields without exposing credentials. Use the active session’s own diagnostics for live lease and managed-sync state.