Privacy and redaction
A capsule is meant to be attached to a public issue, so capture is strict by default. This page lists exactly what it does, from src/redact/ and src/capture/facts/env.ts.
What is recorded
Section titled “What is recorded”- The command: argv (scrubbed), working directory relative to the repo root, exit code or signal, duration, a failure signature and key lines, and the last 200 lines of output (scrubbed).
- The system: OS, architecture, kernel release, distro and version, libc and version, time zone, locale.
- Toolchain: versions of installed runtimes and tools, package managers, lockfile paths and SHA-256 hashes.
- Installed packages: names and versions from
node_modulesand the project’s Python environment. - Repository:
originremote (with credentials stripped), commit, branch,git diff HEAD(scrubbed, up to 200 KB), and untracked file names. - Environment variables: names and states; values only as described below.
What is never recorded
Section titled “What is never recorded”- Values of environment variables outside the allowlist, unless you name them with
--include-env. - Contents of untracked files.
- File contents, other than the
git diff HEADof tracked files. twin never puts.envfiles, your config files or your home directory into the capsule. It reads onlySOLARI_API_KEYandSOLARI_BASE_URLfrom a.env, for its own use, and never passes them to the command. If a tracked file with secrets has uncommitted changes, those changes are in the diff, scrubbed by the rules below. - Credentials in the git remote URL.
- Your home directory path: occurrences become
~.
Capture makes no network calls and uploads nothing.
Environment variables
Section titled “Environment variables”Every variable is recorded by name and state: set, empty or absent. Session and launcher noise that differs on every machine is left out entirely: SSH_*, XDG_*, DBUS_*, GPG_*, desktop and terminal emulator variables (GNOME_*, KDE_*, VSCODE_*, ITERM_*, KITTY_*, TMUX, …), npm_*, and HOME, USER, LOGNAME, PWD, OLDPWD, SHLVL, HOSTNAME, DISPLAY, EDITOR, PAGER and similar.
The value allowlist
Section titled “The value allowlist”Values are kept for these variables, which commonly change program behavior and are safe to share. They are also recorded as absent when unset.
| Group | Variables |
|---|---|
| General | CI, TERM, FORCE_COLOR, NO_COLOR |
| Node | NODE_ENV, NODE_OPTIONS |
| Time zone and locale | TZ, LANG, LANGUAGE, LC_ALL, LC_CTYPE, LC_COLLATE, LC_MESSAGES, LC_NUMERIC, LC_TIME, and any other LC_* |
| Python | PYTHONHASHSEED, PYTHONDONTWRITEBYTECODE, PYTHONIOENCODING, PYTHONUTF8 |
| Go | GOFLAGS, CGO_ENABLED |
| Rust | RUST_BACKTRACE |
Allowlisted values still go through the secret rules (for example, a token inside NODE_OPTIONS is replaced).
Opting in: --include-env NAME
Section titled “Opting in: --include-env NAME”Records that variable’s value too, scrubbed. Repeatable. Use it for values that matter and are not secret: an API base URL, a feature flag.
Comparing without disclosure: --salt TEXT
Section titled “Comparing without disclosure: --salt TEXT”Every other set variable gets hash: the first 32 hex characters of HMAC-SHA-256 of its value, keyed by the salt. Two capsules captured with the same salt can be compared (twin inspect a.json b.json reports whether the values differ) without revealing them. twin never writes unsalted hashes, because hashes of short or common values can be reversed by guessing.
Secret rules (rules version 2)
Section titled “Secret rules (rules version 2)”Every piece of captured text (the output tail, the diff, argv, recorded values and untracked file names) passes through these rules in order, most specific first. A match is replaced by <redacted:RULE-ID> and counted in redaction.scrubbed.
| Rule id | What it matches |
|---|---|
private-key |
-----BEGIN … PRIVATE KEY----- blocks through the matching END line, and keys cut in half by the output tail (base64 lines ending in an END line, or a BEGIN with no END) |
url-credentials |
user:password@ in any scheme:// URL (the scheme and host are kept) |
aws-access-key |
AWS access key ids: AKIA, ASIA, AGPA, AIDA, AROA followed by 16 characters |
github-token |
ghp_, gho_, ghu_, ghs_, ghr_ tokens and github_pat_ fine-grained tokens |
gitlab-token |
glpat- tokens |
slack-token |
xoxa-, xoxb-, xoxp-, xoxr-, xoxs- tokens |
stripe-key |
sk_live_, sk_test_, rk_live_, rk_test_ keys |
google-api-key |
AIza keys |
npm-token |
npm_ tokens |
solari-key |
slr_live_ and slr_test_ keys |
llm-api-key |
sk-…, sk-ant-…, sk-proj-… keys (OpenAI, Anthropic and compatible) |
jwt |
three-part eyJ… JSON Web Tokens |
bearer-token |
the token after Bearer, Basic or Token (the scheme word is kept) |
assignment |
the value in NAME=value, "name": "value" or name: value when the name contains secret, token, password, passwd, pwd, api_key, access_key, private_key, credential or auth (the name is kept). A quoted value is dropped whole, spaces included. Also UPPER_CASE_KEY=value with a value of 8 or more characters, such as ENCRYPTION_KEY=3f2a…, which is often hex |
high-entropy |
random-looking strings no other rule caught (below) |
The entropy heuristic
Section titled “The entropy heuristic”A run of 32 or more characters from A-Z a-z 0-9 + _ = - is replaced when it
- contains a lowercase letter, an uppercase letter and a digit, and
- has a Shannon entropy of at least 4.3 bits per character.
Requiring mixed case and digits excludes hex digests: git SHAs and checksums stay readable. Integrity hashes in lockfile diffs (sha1-, sha256-, sha384-, sha512-) are left alone, because they are public and the diff needs them to apply.
Home directory paths
Section titled “Home directory paths”After the secret rules, your home directory (when it is longer than /) is rewritten to ~ wherever it appears. Paths under anyone’s home, /home/<name>, /Users/<name> and C:\Users\<name>, are collapsed to ~ too, since user names identify people. These are counted as home-path.
What the reporter sees
Section titled “What the reporter sees”The summary printed before writing has a Redaction section with the rules version and a count per rule, for example github-token 1 replaced, or nothing matched. The Environment section lists every recorded value and the number of variables recorded by name only. Press v at the prompt to read the full JSON.
On the maintainer’s side
Section titled “On the maintainer’s side”Replay never makes a secret appear in the sandbox. Variables recorded by name only are left unset, and the report lists them. The maintainer can supply values with --env NAME=value; those come from the maintainer, not from the capsule.
If scrubbing changed the diff (diffRedacted: true), replay notes that it may not apply cleanly. The diff step is optional, so the replay continues either way.