Skip to main content
Capture and upload are two different things. Capture reads the session files your coding agents already write to disk, and never leaves the machine. Upload is a separate step with one gate in front of it.

The gate

A session uploads only when it was launched with OUTERLAYER_WORK naming the work item it is for. A session started without it never leaves the machine, in any tool, however sync is invoked. The gate covers evidence too. An outerlayer emit from a session that was never launched is refused, so the same content cannot leave by a second route. This applies equally to the three ways a session can reach the network: Set "autoSync": false in ~/.outerlayer/config.json to leave every automatic upload to your own command. The daemon reads that setting on every send, so it takes effect without a restart.

What a launched session contains

At the default tier, full, a session ships with:
  • prompts, agent messages and thinking
  • tool inputs and outputs, and images
  • file paths, repository and branch names
  • models, token counts per model, speed and input size, and costs
Speed is the provider’s speed tier for a request, such as standard or fast. Input size is a band of the request’s input tokens, starting at 0, 128,000, 200,000, 256,000, 272,000 or 512,000. The server prices the session from those token counts and stores its own figure. The cost your CLI works out is a local estimate. A Claude Code session also carries the cost Claude Code reports for the session, which the status line records. The server stores it only to compare with its own figure. Choose a lower tier with --tier <tier> on sync, the OUTERLAYER_TIER variable, or "tier" in ~/.outerlayer/config.json. --tier wins over the variable, and the variable wins over the config file. Two lower tiers strip fields on your machine before anything is sent: outerlayer sync --dry-run prints the exact field paths each tier strips. Your organization can set a ceiling of its own. Send a richer tier than the ceiling allows and the server strips the extra fields before storing them.

What is scrubbed before upload

Session content is scrubbed on your machine before it is sent, at every tier. The scrub cannot be disabled.
  • Credentials. About thirty credential shapes are replaced with [REDACTED:<type>]. They include vendor API keys, tokens, PEM private keys, bearer headers, cookies and payment card numbers.
  • Home directory. A home-directory path is collapsed to ~.
  • Local identity. Your OS username, git user.name, git user.email and host name become [DEV_USER], [DEV_NAME], [DEV_EMAIL] and [HOST].
A username or host name shorter than four characters is not replaced outside a home path. A git name shorter than three is not replaced either. Short names are too likely to match ordinary words. To scrub more, add your own strings or patterns under scrub in the config file. The server runs a smaller set of credential patterns again on arrival, plus the home-path collapse. The patterns match known formats, so a credential in an unusual shape can still get through. They favour precision: a missed token can be revoked, but a scrubber that mangles code makes sessions unreadable. The scrub applies to session content and to what a runner reports about a build. Files and text you pass to other commands are sent as you wrote them. That includes outerlayer emit artifact files, outerlayer emit bodies, findings and criteria, and outerlayer work open-pr and outerlayer work comment text.

See it before you send it

It prints:
  • the destination and the tier
  • the exact field paths stripped at that tier
  • image byte totals and a breakdown by repository
  • a row per session, for the first twenty
It makes no network calls. Here it is at the redacted tier, on a machine with no launched sessions:
Add --json to inspect the literal session payloads. For a queued artifact, the JSON lists its file name, kind and session, not its bytes. The dry run shows the tier your machine applies. Your organization’s ceiling is applied on the server, so it does not appear.

Other commands that reach the network

None of these sends session content. This list names the commands people run most. The README in the @outerlayer/cli package names every part of the CLI that opens a connection, and a test checks that list against the code.

Signing in and connecting

  • outerlayer login, without a piped key, sends the dashboard two things:
    • this machine’s host name, cut to letters, digits, spaces and _ - . ( )
    • a one-time public key, made on the spot
    While it waits, it polls the dashboard with the login’s id and nothing else. The dashboard returns a token for your account, encrypted to that public key, plus the gateway address and your email. It names no factory. The private key stays on your machine and decrypts the token once. The token goes into ~/.outerlayer/config.json. A login returned to a script keeps the private key in ~/.outerlayer/login-pending/ until login --check finishes it or the link expires. If this machine already held a login token, login sends that old token to the dashboard once, to revoke it. A key piped on stdin is saved locally and sends nothing.
  • outerlayer connect, and the first step of outerlayer init, send a request with your login’s token: GET /v1/me/factories?repository=<host/owner/name>, naming this checkout’s remote. The gateway answers with the factories you can use, and whether this repository is linked to each. Once a factory is chosen, a second request, GET /v1/apps/<factory id>/git/links, asks which repositories that factory has linked. The choice is saved in the repository’s git directory, never in a tracked file. With a factory key, and with init --local, no request is made.
  • outerlayer work build, list, status, remove and pr, and outerlayer emit artifact, criteria, finding, findings and outerlayer emit <name>, send the same GET /v1/me/factories?repository=<host/owner/name> request before their own. They do this when you are logged in and the repository has no saved connection. If exactly one factory fits, they save it the way connect does. They skip the second request. With a factory key, --app-id or OUTERLAYER_APP_ID, they send nothing extra.
  • outerlayer logout sends the saved token to the dashboard, as the bearer credential of POST /api/cli/logout, so it is revoked. It sends nothing else.
  • outerlayer doctor, with a factory key saved, asks the gateway (GET /v1/repositories/access) which GitHub App permissions each connected repository’s installation has not accepted. outerlayer init runs doctor as its last step. On a runner host, doctor also asks the npm registry for the latest Claude Code version. With builtin:container hooks it also asks the gateway which repository governs each included repository (GET /v1/context/source). It then runs git ls-remote against that repository with the host’s own credentials.

Work items and evidence

  • outerlayer work build, remove, status, list, link-session, pr, open-pr, claim, renew, release, comment and threads start, read and update work items.
    • work link-session sends the item’s number and the session id.
    • work pr sends the session id and the pull request number and repository.
    • work open-pr sends the pull request’s title and the text of the body file you name. A host build sends them to the gateway. In your own session the same text goes to GitHub through gh, and work open-pr then sends the pull request number and repository the way work pr does.
    • work comment sends the comment’s text, and work threads reads comments back.
    • work claim, renew and release record a host’s hold on an item. Run inside a session with no --host, release instead sends the session id and releases that session’s own lease.
  • outerlayer emit artifact sends the file you name, its caption, and any criterion id. It also sends the file’s name, media type, size and sha256, and where it came from: the repository, branch, commit, pull request number, CI run and parsed test results. Inside a recorded session it queues the file instead, and the next outerlayer sync uploads it with the session id.
  • outerlayer emit <name> sends one check’s outcome, and the sentence you give it.
  • outerlayer emit finding, emit findings and emit criteria send the findings or criteria you write, for a work item.
  • outerlayer mcp serve forwards the JSON-RPC messages your editor sends to the gateway.

Hooks in your own sessions

  • The session-start hook, in a governed repository, asks which repository supplies this checkout’s instructions (GET /v1/context/source), and sends the repository name to do it. The files themselves are fetched over git with your own credentials, from your git host, not through OuterLayer. See Share instructions across repositories.
  • The session-start hook, when the repository’s git hooks are missing, runs that repository’s own prepare script. What that script does is the repository’s business.

The runner on a build host

  • outerlayer runner, releasing a claim, sends the host, the outcome, the step the attempt ended in (stage), the exit code of the process that ended it, a reason that is scrubbed and cut to 200 characters, and what the attempt ran in (environment, listed below).
    • The reason comes from the runner itself or from a host’s cleanup or report hook, never from the command’s output.
    • The runner’s own reasons include a failed image build, an out-of-memory kill, the item’s evidence verdict and GitHub refusing a workflow change.
  • The environment a release sends holds:
    • the isolation (container, vm or shared-user)
    • the recipe’s source commit, key and path, and the image id
    • the recipe properties the runner ignored
    • the CLI and agent versions
    • who authored the commits (the GitHub App or the host), and whether the person who asked for the build was credited
    • for a container build: the hosts the build reached (up to 200, with a count of the rest), and the first 200 addresses the tunnel refused with their range (with a count of the rest)
    • the variable names the runner’s config passed in, the names of the destinations the build called, and which kinds of repository token the build used (read, push)
    It never sends a token or a variable’s value. It never sends a destination’s address, header or secret: a destination’s secret is read on your machine and used there, and only its name is recorded. It sends no part of any request the build made beyond the host name.
  • outerlayer runner init --vm on a Mac runs limactl. Lima downloads a Linux VM image, and inside the VM apt downloads Node and npm downloads the CLI.
  • outerlayer runner check asks the gateway (GET /v1/runner/host-key) whether the runner key can claim work.
  • outerlayer runner image fetches the repository’s default branch with the host’s own git credentials. Docker pulls base images and features with the host’s own docker login. The build also downloads pinned gh and tini releases from GitHub.
  • outerlayer runner, on every poll, sends a heartbeat. It holds the host name (runner.host, which defaults to this machine’s hostname), the CLI version, the runner protocol (which version of the gateway contract the CLI was built for), the poll interval, and the slots in use and the total. It holds the result of that poll’s own request for work — success, or an error code and message. The error is only what the platform already returned to this host, or a fixed sentence when no error came back from the platform. Once the runner has updated itself, it adds lastUpdate: the two CLI versions, how it ended (updated, rolled_back, verification_failed, not_published or retrying), a short reason and the time it ended. It adds one field only while the runner holds off new work for want of disk, lowDisk: the free and needed disk space in bytes. While a check of the runner’s own stops it claiming work, it adds blocked: one sentence of at most 500 characters that names the cause. The causes are a missing /dev/kvm for a microVM build, no Claude credential, a command still set to the placeholder runner init writes, builds that do not fit in memory, and an image builder that is not ready or not usable. Depending on the cause, that sentence can include the host’s usable memory in GiB, the /dev/kvm path and Docker’s own error text. It is not scrubbed. Nothing else about the work item or the machine is sent.
  • outerlayer session-sender runs only inside a runner’s build. It streams the sessions that build launched to its own work item, with the build’s item key. It sends no other session on the machine.
  • outerlayer runner, before a build’s command starts, runs git ls-remote and git fetch in the job’s workdir with the host’s own git credentials, to put it on the branch the claim named. Nothing about the job is sent to OuterLayer that way.
  • outerlayer runner, for a container build, asks the gateway for a read token and a push token limited to the item’s repository (POST /v1/work-items/{workItemId}/claim/tokens). It sends git’s fetch and push requests for the build to github.com itself, with those tokens. The build’s git traffic goes from the runner, not from the build. When the attempt ends, the runner revokes each token at GitHub. No token goes to OuterLayer. The read token is written to the build’s gh/hosts.yml on your machine so gh can use it, and is deleted with the build. The push token stays in the runner’s memory.
  • outerlayer runner, for a container build, asks the gateway which repository governs the item’s repository (GET /v1/context/source). It fetches that repository on the host with the host’s own git credentials. The request names the item’s repository and nothing else. The build receives that repository’s files, never a credential for it.
  • outerlayer runner, for a build it provisions itself, serves two built-in destinations: gateway and claude. The build sends its requests to your gateway and to Claude’s API (api.anthropic.com) through the runner. The runner adds the attempt’s item key and the host’s Claude credential on the way out. The credential goes to Anthropic only, and is read from the runner’s own environment. A container or microVM build never has it in its environment or files. A builtin:process build runs as the runner’s user, so it can read the credential from the runner’s files.
  • outerlayer runner, for a microVM build (builtin:vm), downloads a pinned Firecracker release, a guest kernel and a small guest helper from github.com the first time it needs them. It checks each against a sha256 pinned in the CLI. The runner sends nothing in those requests. The build’s connections leave the microVM over vsock to the runner’s tunnel, which applies the same refusals as for a container build. The microVM has no network device of its own.
  • outerlayer runner, when a build’s command exits 0, runs the repository’s host checks. It sends each one’s result (POST /v1/emitted-results): the check’s name, pass or fail, and for a fail the exit status and the last line of its output, scrubbed. It then reads the work item once more, for its evidence verdict, and sends nothing but that request.
  • On a runner host, any command that uses the saved runner key signs each request with the host key. The signature covers the request’s method, path, query and body, and adds nothing else.
  • outerlayer runner start, when the gateway names a higher CLI version than the one running and no build is in flight, calls the public npm registry (registry.npmjs.org). It asks for @outerlayer/cli: that version’s metadata, its tarball and its provenance attestation. Those requests carry no credentials and nothing about the host or the factory. It then runs npm install and npm audit signatures. These fetch the package’s dependencies too, and they use this user’s npm configuration, including any registry token in ~/.npmrc. Set runner.autoUpdate to false to stop them.
Three paths are proved to make no network calls at all, each tested with every outbound call replaced by a stub that throws:
  • outerlayer daemon --once
  • a session start in a repository nothing governs
  • the daemon’s handling of a session that was never launched

Failures you cannot see in the session

Two failures never show up in the session itself: an OUTERLAYER_WORK value that was refused, and a work link-session that could not reach OuterLayer. Both are appended to ~/.outerlayer/spool/hook-errors.log, and the next session start tells you so.