Local security model

Copy safe project defaults and look up public, control, code-execution, storage, logging, and sharing boundaries.

localhost2137 assumes a trusted developer project on a developer machine or isolated CI worker. Start by excluding the default storage root:

.gitignore
.localhost2137/

Use clearly fake local credentials. The checked config uses xoxb-local-crash-course and local-crash-course-signing-secret; neither grants access to an external account. Use synthetic records as well. Persistent worlds and failed test cleanup can retain their contents.

pnpm exec localhost doctor --json
pnpm exec localhost env --instance dev --format json

doctor is read-only but can contain local paths and instance metadata. env intentionally prints application-facing local credentials. Review both before sharing; neither output includes the runtime control token.

Surface map

SurfaceIntended callerProtectionAuthority
/{instance}/{service}/*Application or provider SDKAuthentication implemented by that pluginRead or mutate one emulated service world
/_/v1/*, except healthCLI, test harness, trusted automationDaemon bearer token; browser-origin requests rejectedCreate, reset, seed, inspect, advance, destroy, invoke operations
/_/v1/healthLocal discoveryLoopback binding onlyReturn runtime protocol health
Files below storage.dirRuntime and trusted local toolsLocal account and filesystem permissionsHold discovery metadata, local credentials, manifests, plugin state

The health route is the only unauthenticated control route:

curl --silent http://127.0.0.1:2137/_/v1/health
response
{ "data": { "status": "ok", "version": "v1" } }

Loopback is a network boundary, not authorization between local processes. Another process running as the same user may reach public routes and read project files. Do not treat 127.0.0.1 as tenant isolation.

Public routes use plugin credentials

The runtime bearer token does not protect provider-shaped routes. They exist so an application can use its ordinary SDK or HTTP client. Each plugin owns its fake local credentials, request authentication, callback signatures, and provider-shaped rejection behavior.

A browser may call a public route when normal browser networking permits it. The control API's Origin rejection does not apply to application traffic. Do not put privileged setup operations on a public plugin route, and do not mistake a fake provider token for protection against hostile local code.

Never copy a production provider key into plugin config. A local emulator does not need external account authority. If a real key was used, revoke it at its source; resetting the local world cannot rotate it.

Control authority stays outside the application

Except for health, a control request needs the current daemon bearer token. Any request carrying an Origin header is rejected, the control API does not enable CORS, and JSON mutations require JSON content type with a maximum body size of 64 KiB.

The daemon creates a new random token at startup and stores it as control-token below storage.dir. The active descriptor, token, and generated .env use owner-only 0600 mode where POSIX permissions are available. File mode is defense in depth; the storage root and local account remain the trust boundary.

The CLI and test client read the token automatically. Direct authenticated HTTP is documented once in Plain HTTP control. Do not:

  • add the token to plugin connection values or application environment;
  • pass it through localhost run;
  • expose it to browser code, logs, traces, screenshots, or reports;
  • cache it across daemon restarts;
  • commit or package the storage root.

If it leaks, stop and restart the daemon, remove the retained copy, and inspect affected instances. Rotation prevents later use; it does not undo completed mutations.

Config and plugins are trusted code

Loading config runs its imports. Plugins execute with the privileges of the runtime's Node process. There is no process, filesystem, or network sandbox between a plugin, another instance, and the host user.

Treat plugin installation like any development dependency that executes code: review its source and dependency/version policy, install it explicitly, keep config imports focused, and do not load an untrusted change merely to inspect metadata. Run untrusted code in a separate OS or CI boundary.

The plugin storage helper rejects absolute paths and traversal outside its service data directory. That prevents accidental helper misuse; it does not stop a plugin from calling Node filesystem or network APIs. Tracked fetch makes work observable, not firewalled. Instances isolate plugin-owned data and lifecycle, not code execution or permissions.

Storage and logs can contain sensitive data

storage.dir may contain provider-shaped records, callback payload data, generated application connection values, the control token, runtime discovery metadata, manifests, and plugin databases. A custom root needs the same ignore treatment as .localhost2137/.

Structured-log attributes redact sensitive-looking keys and omit body-like values. This exact tested result shows the transformation:

tested log attributes
{
  "authorization": "[REDACTED]",
  "circular": "[CIRCULAR]",
  "nested": {
    "payload": "[OMITTED]",
    "token": "[REDACTED]",
    "visible": "safe"
  }
}

Ordinary string values and the log message pass through unchanged. A plugin message or an attribute such as note: "token is ..." can expose exactly what its author supplied. Logs are bounded diagnostic evidence, not a privacy control or audit store.

Review localhost logs --json, localhost doctor --json, failure output, and retained storage paths before posting them. Copy this source-synced section into a report and verify every item:

share-check.md
## Removed before sharing

- Runtime control token
- App-facing connection credentials and provider secrets
- Personal data and unrelated request bodies
- Unrelated environment variables and log entries

Destroy only explicitly owned worlds. Remove an explicitly owned temporary directory only after its runtime has closed. Never turn cleanup into a broad or wildcard path.

Application environment is not containment

localhost env renders plugin connection values. localhost run overlays those values on the child's inherited environment. Neither command injects the control token, and run does not sanitize inherited variables. This reduces accidental exposure through localhost2137's connection projection. It does not contain a same-user process: a Node process with the user's filesystem privileges may read project files, including the stored control token, and call the control API.

A browser test needing privileged arrangement should perform it in the Node test harness or CLI, then pass only provider-shaped IDs or application-visible state into the browser. Origin rejection is an additional guard against browser-origin control traffic, not a sandbox or hostile-local-code boundary.

Run hostile application code under a separate OS/process account or in an isolated CI boundary appropriate to the threat model. Merely starting it as another process under the same user is not isolation.

Runtime boundaries identifies claims requiring another environment. Diagnose a failing scenario shows how to preserve useful evidence without deleting broad directories or publishing secrets.