> ## Documentation Index
> Fetch the complete documentation index at: https://docs.pikopod.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Security

> How pikopod behaves in a production request path, and what leaves the machine.

The observing agent is a reverse proxy that sits in a production request path, and it writes what it observes to disk. Both facts drive everything below. To report a vulnerability, see the [security policy](/project/security-policy).

## The data plane never fails closed

The proxy serves first and observes afterwards. Observation runs asynchronously behind a bounded queue, every capture stage is panic-isolated and counted, and the proxy never retries a request. An automatic retry in front of a payments API is a double-charge window.

If pikopod's own machinery breaks, your traffic still flows. That is the contract, and it is why observation failures increment a counter instead of surfacing as an error. See [Data plane safety](/observe/data-plane-safety) for the measurement.

## Redaction happens before the disk, not after

Values are classified and rewritten on the way in, not cleaned up later:

* **Credentials** become typed placeholders.
* **Identifiers** become deterministic, format-preserving tokens, so `tok_8f3a91` stays token-shaped and path templates still work.
* **Names and similar PII** are dropped.
* **Free text and unknown strings** are dropped. The classifier fails closed on any *string* it cannot place.

**The exception, stated plainly: numbers, booleans and nulls are kept unless their key names them as sensitive.** A number carries no shape signal. `5000` could be an amount, an account number or a timestamp, so key names are the only evidence, and an unrecognised key passes the value through. Keys that look like secrets, identifiers, card data, phone numbers or expiry dates are still substituted or tokenized whatever the JSON type, because a card number sent as a number must not ride through on its type. But `"amount": 245000` under a key pikopod does not recognise is written verbatim.

This is a deliberate trade. Dropping every unrecognised number would discard most of what makes a baseline useful, and it is the one place redaction is not fail-closed. If that matters for a field, check it with `pikopod inspect` before you trust the recording. `volatile_fields` does not keep a field out of learning; it only stops its values from being tracked.

Detection is generic: key names, value shape, entropy. Never a list of provider-specific prefixes, so it does not silently stop working when you add a provider nobody anticipated.

**One thing an imported contract changes, precisely.** When an upstream has an imported spec, a response field whose schema declares an `enum` keeps a value on disk only if that value is one of the declared members. `currency: NGN` survives because the provider published `[NGN, USD]`; the same field carrying `GHS` or a free-text error is classified exactly as it would be without the spec. Only `EXPLICIT` and `DERIVED` enums count. An enum the model extracted from prose unlocks nothing, because relaxing redaction on a guess is not recoverable. Request bodies, headers, paths and every other field are untouched. `pikopod up` prints how many fields this applies to.

Verify it yourself rather than trusting this page:

```bash theme={null}
pikopod inspect
```

See [Redaction](/observe/redaction) for the rule set.

## The salt

Tokenization uses an HMAC keyed by a per-install salt stored at `<data_dir>/.salt`, generated on first run. The salt is what makes tokens correlatable **within** your install and meaningless outside it.

The file must be mode `0600`. pikopod refuses to start if it is group- or world-readable, because another local user who can read it can correlate every token you have stored:

```bash theme={null}
chmod 600 <data_dir>/.salt
```

Deleting the salt regenerates it. Existing tokens will no longer correlate with new ones, so baselines built on the old salt are effectively reset. Treat the salt as state worth backing up alongside `data_dir`, and exclude it from anything you share.

## Listener safety

Both servers bind `127.0.0.1` by default.

Binding any non-loopback address **requires a token**, supplied through `PIKOPOD_TOKEN` or a `token_file`, never through a command-line argument, which is visible to every user on the box via `ps`. Tokenless listeners also reject requests carrying a foreign `Host` header, which blocks DNS rebinding from a browser on the same machine. The token authenticates the whole control surface and is stripped from every request before it can travel further.

The sandbox control plane under `/_pikopod/` is unauthenticated on loopback, like the rest of the sandbox. It controls a fake, never a provider.

Serve over HTTPS with [`tls`](/operations/configuration#tls). Both certificate and key, or neither.

## What leaves the machine

pikopod initiates no network traffic on its own. Everything outbound is something you configured, using your own credentials:

| Destination                               | Why                                      | Carries                                                                                                                  |
| ----------------------------------------- | ---------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ |
| Your upstream targets                     | The proxy forwarding your traffic        | Your requests, unmodified                                                                                                |
| Your Slack webhook                        | Alerts                                   | Finding structure and field paths                                                                                        |
| GitHub or GitLab                          | `pikopod pr`                             | The report you asked it to post                                                                                          |
| Your model provider                       | `scenario create`                        | Your prompt, plus the operation inventory of the imported spec                                                           |
| Your model provider                       | `fix`                                    | The drift event, plus **excerpts of your source**: up to 5 matched regions per file, with 4 lines of context either side |
| Your model provider                       | `import --spec <docs-url>`, rung 4 only  | Up to 240 KB of the **provider's** documentation prose, in 36 KB batches                                                 |
| A `spec_source` URL                       | The declared-drift watcher               | Nothing. It fetches.                                                                                                     |
| A documentation page and what it links to | `import --spec <docs-url>`, rungs 1 to 3 | Nothing. It fetches. See below.                                                                                          |

Every model row requires **your own key**. With no key configured, none of those three paths runs at all: `fix` refuses, `scenario create` refuses, and a docs-URL import that needs rung 4 returns an error telling you so. pikopod never proxies a request through infrastructure anyone else operates. Your relationship is with the model provider you configured.

There is no telemetry, there are no accounts, and there is no license check.

## Importing from a documentation URL fetches more than one page

If `--spec` points at an HTML page rather than a spec, pikopod climbs a ladder. You should know what that does before pointing it at an internal documentation host:

1. **Linked documents.** It follows `href` and `src` links from the page to anything that looks machine-readable. **There is deliberately no same-host restriction**, because documentation sites legitimately host their OpenAPI document on a different domain. That means a page you do not control can direct the fetcher at a host of its choosing. The risk is bounded by size and content-type gates and by the fact that anything fetched must parse as a spec to be used, but if your threat model includes SSRF from an untrusted docs page, do not point `--spec` at one. Fetch the spec yourself and pass a local path.
2. **Embedded documents.** The page itself, plus at most one hop into the site's own reference index. Same host.
3. **Well-known spec paths** on the same host.
4. **Model extraction.** Only with your key. Assembles up to 240 KB of the site's prose, preferring its `llms.txt` index, and asks your model to write an OpenAPI document from it. The result is marked `LLM_EXTRACTED` and imported as a draft.

Rungs 1 to 3 are deterministic. Rung 4 is not, which is why its output carries provenance that follows it through every later decision. See [Importing from a docs URL](/sandbox/docs-url-import).

## Third-party specs are untrusted input

A fetched specification is bytes from someone else's server. Parsing is bounded on size (32 MiB), depth and reference expansion, runs panic-isolated, and a document that fails to normalize is reported as an error. It never silently retires the watcher. Remote and absolute `$ref`s are refused outright; see [Multi-file specs](/gate/multi-file-specs).

## Verifying a release

The canonical verification command lives in [Installation](/getting-started/installation#verify-a-release), in exactly one place, because a copy that drifts out of case or loses its anchor still runs and still passes.
