Security and privacy
This page covers only what you can do: know where traffic goes, put credentials in the right place, know where data is kept, and rotate. tansr’s internal defenses (permission engine, protected paths, hard security red lines, audit chain) are laid out in the guides for each form; here they are cited only where they touch your own actions. Version baseline: tansr CLI 0.10.3, @tansr/sdk 0.19.0, @tansr/serve 0.15.1 (all released 2026-10-10).
Egress: where traffic goes and how to narrow it
Section titled “Egress: where traffic goes and how to narrow it”What the CLI connects to
Section titled “What the CLI connects to”| Destination | When | Can it be turned off |
|---|---|---|
The tansr platform gateway api.tansr.com |
After signing in to platform mode: model calls, bundle fetch, heartbeat, usage reporting, balance | Set TANSR_ESCAPE_LOCAL=1 for fully local mode and it is disabled as a whole |
Update check: the npm install queries the npm registry registry.npmjs.org; the single-file executable (SEA) queries the signed pointer on the official distribution point bash.tansr.com |
Only when you explicitly run /update or tansr update; never checked automatically at startup |
TANSR_UPDATE_CHANNEL=none or TANSR_NO_UPDATE_CHECK=1; companies can point to a self-hosted manifest with TANSR_UPDATE_MANIFEST_URL (when present, both forms connect only to it) |
| The model providers you configure | providers.*.baseUrl in fully local mode |
Your decision |
| The MCP servers / your own search and media endpoints you configure | Tool calls within a session | Your decision; project-level MCP servers must be trusted explicitly |
| Any URL the model reaches through tools (WebFetch / Http) | Within a session | Governed by permission rules; a deny rule can disable the whole tool |
The recommended baseline for unattended deployments (tansr serve / headless with permissions set to auto) is an egress allowlist: allow only necessary destinations such as the model API, and deny by default beyond DNS and already-established connections; combine it with a container or a least-privilege dedicated user, and limit filesystem writes to the working directory. Reference templates are in the repository under deploy/serve/.
Two user-visible defenses already in place: a single command that both references a secret file (.env, keys, .aws/credentials) and performs an outbound action (curl / scp / git push …) is always refused, and no mode or rule can override that; and once a session has read a secret file, every later outbound action turns into a prompt (session taint). Since tansr CLI 0.6.0: all egress (WebFetch / Http / hooks / audio download) goes through one unified exit that resolves DNS first and then rejects private, loopback and link-local addresses, so fetches aimed at internal or metadata addresses are refused with a reason; and the credential environment variables the Http tool and hooks may reference are narrowed to the http.allowedAuthEnvVars allowlist — nothing outside it is sent.
What the SDK and the session service connect to
Section titled “What the SDK and the session service connect to”- With the token source,
@tansr/sdkconnects only to thebaseUrlyou pass (the platform gateway) and the MCP servers you configure explicitly; the SDK never scans the user’s home directory and never has a built-in search endpoint. - The upstream chain of
@tansr/serveisappid/appkey → POST /v1/app-tokens → GET /t1/config + POST /t1/heartbeat → POST /t1/exchange, all againstapiBaseUrl; plus the end-of-turn webhook URL you configure (outbound requests never carry any authentication header — authenticity is proven by the signature) and the optional cold-tier object storage. - Once a mobile app enables web fetch (WebFetch) or HTTP requests (Http) in the console (
@tansr/serve0.12.0 and later, off by default), those two tools access the target URL directly from the host running the session service and follow the same egress rules: hosted Http cannot reference credentials from host environment variables, and the model cannot lift the host’s private-network limits; if you use an egress proxy, restrict private-network targets on the proxy as well. See Public-network tools for mobile apps. - The mobile SDKs and
@tansr/api-clientconnect only to your session service’sbaseUrl, never directly to the platform, and never issue those tool requests from the device.
The unified call surface /api: headers, closure and legacy prefixes
Section titled “The unified call surface /api: headers, closure and legacy prefixes”Since @tansr/serve 0.13.0, a deployment with options.v2 present mounts the unified call surface /api at the head of the chain on the same port (manifest revision 7 in 0.14.0). Only a few points are security-relevant:
- Identity comes from
Authorizationonly./apiand/v2share the sameauthenticatehook that decidesendUserId; the four response headerstansr-contract/tansr-manifest-revision/tansr-domain/tansr-schema-hashand the envelope’straceId/requestIdnever carry endUserId, tokens or message content. No identity field in a request body is treated as identity. The CLI formtansr serve --v2partitions by the same Bearer plus thex-tansr-end-userheader; that header may only be written by your gateway after your own login, never filled in by the client. - Request headers are explicit negotiation, not authentication factors.
tansr-session-family,tansr-closure-id,tansr-event-envelope,Idempotency-Key/If-Match/deadline(the last three since 0.14.0) on a request they do not apply to yield400 invalid_requestrather than being silently ignored; without any of them, behaviour is byte-for-byte unchanged. - Capability closure:
GET /api/sessions/:id/capabilitiesfederates the existing capability sources of each domain read-only into one three-state operation table (enabled/disabled/unavailable) plustansr-closure-id; it introduces no new authorization engine — operations inside the closure are still decided by the domain handler against app capability flags, user authorization, session state and approvals. A write may carry the lasttansr-closure-idas a precondition: a changed closure yields412 precondition_failed(rediscover; rediscover first, never hard-replay with the old value); calling an operation that isdisabledyields403 capability_unavailable. Unknown sessions, other users’ sessions and sessions that exist only in the archive host all answer404 not_found, without leaking existence. /v1is off by default: with the facade present,/v1/sessions*,/v1/statusand/v1/approvals/:id/credentialanswer 404 by default (always off when an SDK2 explicit host is present). The escape hatchtansr serve --legacy-v1/TANSR_SERVE_LEGACY_V1=1is deprecated since 0.14.0 / CLI 0.10.1 (removed in 1.0.0; enabling it emits thewarneventserver.deprecated_option); approval credential submission moves toPOST /api/approvals/:id/credential. Health checks use/healthz/readyz; stop probing/v1/statuswith the/v1Bearer.- The retirement window for the legacy
/v2and/v3/*prefixes is configurable (@tansr/serve0.15.0 / CLI 0.10.2 and later, effective when the facade is present):TANSR_SERVE_LEGACY_PREFIX=readwrite|readonly|off(factory optionlegacyPrefixes) —readwriteis the default (still so in 0.15.1) and accepts everything on the legacy prefixes; underreadonly, operations the manifest registers as writes answer410 goneon the legacy prefixes (messagepoints to the corresponding/apioperation) while reads and streams still work; underoffevery legacy-prefix request answers404 not_found. OnceTANSR_SERVE_LEGACY_SUNSET(legacyPrefixSunset) names the planned retirement time, legacy-prefix responses carry aSunsetheader in addition to the existingDeprecation/Linkheaders. Mixing the legacy prefixes and/apiwithin one session logs thewarneventapi.legacy_prefix_mixed; a rejected legacy-prefix request logsapi.legacy_prefix_rejected— both staying at zero is the evidence that the window can be narrowed. When the default changes is covered in Migration and retirement timeline. - In the unified error envelope
messagehas no literal guarantee anddetail.pathis rewritten to the requested/apipath; the internal/v2//v3prefixes and host paths do not leak. Headers and the code table are in Unified API headers and error envelope.
Isolation of system tools and terminal tools (SDK 2.0 remote profile)
Section titled “Isolation of system tools and terminal tools (SDK 2.0 remote profile)”SDK 2.0’s “unified core, thin terminals” splits tools by where they execute, and neither side can execute for the other:
- System tools run on the session-service host: WebFetch / Http / WebSearch / media tools and the MCP adapter run inside the serve process under the host’s egress rules (private-network limits, credential allowlist, the same on the proxy); the MCP adapter reuses the host’s existing connections and cannot start processes from model parameters, and the model cannot lift egress limits.
- Terminal tools run only on the bound device: file read / list / write / edit and bound-workspace memory (controlled primitives such as
fs.inspect/fs.read/fs.list) exist only after the application explicitly configuresexecution.version = 'bound-device-v1'through the management API and grants them inboundDevice.tools; legacy application types and legacytoolsflags never imply terminal authorization. Effective tools = application ceiling ∩ user policy ∩ request narrowing ∩ device primitives; the original Read / List permissions and user approvals still apply, and a configured directory cannot authorize on the user’s behalf. - Memory and attachments never fall back to the host: the trusted host’s
execution.memoryFor(scope)selects only relative directories inside the bound workspace and never reads serve’s home / env / cwd; missing capability, denial, disconnection or an oversized source only degrades the current turn and never falls back to reading server files. The URL of an external attachment sourcetrustedAttachmentSourceForis only a lookup key: SDK / serve never fetch automatically and never let the model or the client turn an address into a file or network permission. - Durable execution records before effects: the executor persistently claims an operation before executing and saves the result before returning; reconnecting after a disconnect never redoes an operation whose outcome is unknown (SDK side:
GateDecision.executionTarget/ToolContext.executionTarget, since@tansr/sdk0.18.0; always absent for in-process SDK). - A trusted host module is trusted like the deployer’s own code:
tansr serve --v2 --host-module <absolute.cjs> --host-module-sha256 <sha256>(CLI 0.10.0 and later) verifies only the digest of the entry file’s raw bytes; it does not sign the dependency graph and is not a code sandbox. Path and digest always come from your deployment pipeline, never from the model or over HTTP.
Credentials: where they go and where they never go
Section titled “Credentials: where they go and where they never go”| Credential | Represents | Right place | Never |
|---|---|---|---|
| Personal access token (PAT) | You | Generated in the console, then fed only to tansr init (masked input) or the TANSR_PAT environment variable in CI |
Shell history (--pat on the command line leaves a trace), plaintext in scripts, the repository |
| This machine’s derived app key | This device of yours | Written by tansr init to the OS keychain (macOS Keychain / Linux secret-service / Windows DPAPI); falls back to a 0600 file with a clear warning when unavailable |
You never touch it; the credential-storage section of tansr doctor shows where it landed |
| Model-provider API key (fully local mode) | Your provider account | An environment variable; the config file holds only the variable name in auth.env |
settings.json, error messages, /config output (they never carry the value) |
| App key appid / appkey | Your app | Environment variables of your backend process (a Node process or @tansr/serve) |
Client devices, logs, error responses, the repository (.env goes in .gitignore) |
Short-lived app_user token |
One of your end users | Electron main-process memory; inside the @tansr/serve process |
The renderer process, persistent storage; mobile never touches it |
| Your own login token (mobile) | Your user | Provided per request through authProvider; the SDK does not persist it |
SharedPreferences / DataStore / UserDefaults / logs |
Session-service /v1 Bearer (--token / TANSR_SERVE_TOKEN) |
The operator | An environment variable | Unrelated to the /v2 / /api surfaces; do not reuse it for end users; /v1 is off by default in --v2 deployments |
The --host-module-sha256 digest |
Your deployment pipeline | Generated next to the .cjs build artifact and shipped together with it |
Never computed at runtime, never supplied by the model or over HTTP |
Webhook secret |
Your push backend | Environment variables on the session service and on the receiver, separately | Logs; the engine never prints it |
Three red lines worth repeating: the appkey never leaves your backend; the app_user token never leaves the session service; the session id is not an authentication factor.
The CLI adds one more layer of redaction discipline as a safety net: keys in reports, logs and exported transcripts are always redacted to the form sk-... plus the last 4 characters; the output of tansr sessions export can go straight into a ticket.
The trust gate for project-level hooks: zero execution by default
Section titled “The trust gate for project-level hooks: zero execution by default”Available since tansr CLI 0.7.0. A repository’s .tansr/settings.json / .tansr/settings.local.json may declare command and http hooks — code that could run just because you cloned a repository — so the CLI places project-level hooks behind the workspace trust gate and does not run them by default:
- The trust record lives in the user layer,
~/.tansr/trust.json; there is no separate grant file. Workspace recorded as trusted → project-level hooks run as usual; no record, or recorded as untrusted → project-levelcommand/httphooks are never executed (zerohook.*events) and stderr gets a one-line notice; user-level and managed-level hooks are unaffected. - TUI: the first time you open a directory without a record, user-level / managed-level
SessionStarthooks still fire before the first screen, while the project-levelSessionStartis deferred until you choose “Trust” in the trust dialog and then fires exactly once (Decline / Esc: not fired, and the session enters the tightened mode). - The four unattended entry points (headless
-p,tansr serve,tansr acp,tansr mcp serve) have no interactive surface and honour only the record the TUI persisted: a repository you trusted in the TUI gets its project hooks automatically when CI or an editor connects. If there is no record and you really need them for this run, pass--trust-project-hooks(same flag name on all four entry points) or setTANSR_TRUST_PROJECT_HOOKS=1(other values are ignored). It applies only when there is no record, only to this run, is never written totrust.json, and cannot override a recorded distrust. - The criterion is the configuration anchor chain, not the current directory: the anchor is the nearest ancestor of the start directory that contains
.tansr; if the anchor or any of its ancestors is recorded as untrusted, hooks stay at zero execution even when a subdirectory is recorded as trusted, and neither the flag nor the variable can override that. Trust keys are normalised by real path (realpath): a junction, symlink,substdrive, 8.3 short name or mapped drive pointing at the same workspace is the same record; if the real path cannot be resolved the literal path is used and is not treated as trusted — a trust recorded historically under an alias path has to be granted once more. Neither TUI/cdnor a mid-session cwd switch over/v2re-adjudicates. - In project-level
httphooks the whole-value${VAR}form in request headers is not expanded and is sent literally (repository configuration never reads your environment; the request is still sent, not rejected); user-level / managed-level hooks expand as before. Hooks shipped by project-level plugins have already passed per-plugin trust and are not gated again. - Project-level MCP servers and plugins follow the same rule (since 0.7.0): the four unattended entry points also honour only the trust records the TUI has persisted for MCP servers and plugins declared by the repository, in this order — persisted deny (matched by definition fingerprint) > persisted allow >
TANSR_TRUST_PROJECT_MCP=1/TANSR_TRUST_PROJECT_PLUGINS=1, which only apply when there is no record or the fingerprint no longer matches > pending. A changed MCP definition counts as a mismatch, and legacy literal-path records are honoured only for deny, never borrowed for allow. Deployments that relied solely on the environment variable to bypass a persisted deny are refused from 0.7.0 on — re-grant trust in the TUI, or delete that deny record.tansr acpshares the same plugin discovery / trust / signature verification with the other entry points; untrusted plugins are never loaded and the notice goes to stderr (stdout carries protocol frames only). - SDK embedders (since
@tansr/sdk0.14.0):createSession({ trustProjectHooks: true })means the host grants trust on the user’s behalf; the defaultfalsemeans project-level hooks are never executed and emit zerohook.*events. The SDK never reads the CLI’strust.jsonand never persists trust — the embedder owns that responsibility. When trust is not granted but the project does declare hooks,onWarningreceives oneproject_hooks_untrusted(detailcarriescwdand the layers found,projectShared/projectLocal); withoutonWarningit falls back to a recoverableturn.error { scope: 'sdk.notice' }. Single-turnquery()has no trust bit, so project-level hooks are never enabled there.
Data retention: where, how long, who deletes
Section titled “Data retention: where, how long, who deletes”Locally (the CLI)
Section titled “Locally (the CLI)”- Sessions, config cache, usage queue and project memory live under
<storageRoot>/(default~/.tansr, redirectable withTANSR_STORAGE_ROOTor the overall rootTANSR_USER_HOME); after signing in to the platform, new data is isolated per account under<storageRoot>/identities/<account fingerprint>/. - One directory per session:
journal.jsonl(hash chain),meta.json,attachments/, oversize artifacts inspill/; image bytes are cached under<storageRoot>/image-cache/<sessionId>/; long Shell output goes to<working directory>/.tansr/shell-output/; TTS audio goes to<working directory>/.tansr/artifacts/<sessionId>/. - Nothing is deleted automatically. Clean up with
tansr sessions prune(a dry run listing only by default;--applydeletes; the retention policy is the union of--keep-days, default 90, and--keep-last, default 500; today’s sessions and sessions in use are never deleted; corrupted directories need--include-broken).tansr logoutdoes not delete identity-store data. - The
metrics.jsonwritten at the end of a session holds only counts and ratios — no paths, commands or rule text. Outbound telemetry is off by default: the current version has no outbound reporting channel at all. - The platform does not store terminal session content; it only meters; the platform session registry has a 30-day inactivity lease, after which sessions are archived.
The session service (@tansr/serve)
Section titled “The session service (@tansr/serve)”- Session history lands in the
storedirectory you configure,<dir>/agent-sessions/<endUserKey>/sessions/<sessionId>/;endUserKeyis a hash of the end-user id, not an identity. A cold tier can be mounted on your object storage; segments are plaintext JSONL (compressed) — encryption, key management, residency and access control are yours (thepolicy.transformhook). - Short in-memory windows: terminal records are kept for 30 min, idle sessions are written to disk after 24 h;
DELETE /v2/sessions/:id(same as/api/sessions/:id) closes, it does not delete. With no retention configured the store keeps everything by default; the storage retention sweep (deleting dormant sessions by age in days and per-user count) is available since @tansr/serve 0.8.0 (factory optionsessions.retentionor two environment variables — see Retention and governance). Real deletion isstore.delete. - Since 0.14.0 the
Idempotency-Keyreceipt cache is in-process memory (256 per user / 4,096 global / 10 minutes), isolated byendUserIdand gone with the process; an explicitly assembled SDK2 archive host stores archives under a “single authorized source” strategy — it is not a backup and does not synchronize elsewhere, and entries are deleted only by an explicit host call. - Structured log fields never contain tokens or message content (default level
warnsince 0.13.0; every level is redacted the same way, body keys are always[redacted]); metric labels never contain sessionId / endUserId. - Permission request frames sent down to clients never carry full tool arguments, only a target summary; webhook payloads never carry message content or credentials.
On the device
Section titled “On the device”The Android / iOS / HarmonyOS SDKs cache no credentials; view state is in memory; whether to keep the sessionId locally (for resume after a cold start) is your decision — store only the id, not content, not credentials.
Rotation and revocation: how each credential is replaced
Section titled “Rotation and revocation: how each credential is replaced”| Credential | How to rotate | Blast radius |
|---|---|---|
| PAT | Create a new one in the console, sign in again locally with tansr init --fresh, then delete the old PAT in the console |
A PAT is never revoked by tansr logout — other devices may still be using the same one |
| This machine’s derived app key | Rerun tansr init to rotate and extend (near expiry the session shows a “key about to expire” notice); tansr logout clears the local copy and does its best to deactivate it remotely; for a lost device, deactivate its device key manually in the console |
Every device holds its own key (named by hostname + device id); they never replace each other |
| App key appkey | Rotate in the console (POST /v1/apps/:id/rotate); update the backend’s environment variable and restart the process |
Cuts everything off as a last resort: the old appkey is invalidated immediately |
| One end user’s tokens | Revoke per end user in the console or via the management API (POST /v1/apps/:id/app-user-revocations); all of that user’s existing tokens are rejected at once. Ending their login state in your own system also cuts supply naturally (token TTL 60–86400 s, no renewal) |
Affects only that user |
| The whole app | Deactivate the app in the console | Everything downstream is rejected |
Session-service /v1 Bearer |
Change TANSR_SERVE_TOKEN / --token and restart |
/v1 callers must switch at the same time |
Webhook secret |
The receiver accepts both the old and the new value first, then the session-service side is switched, then the old value is retired | Zero lost deliveries in between |
| Your own login token (mobile) | Belongs to your login system; on 401 the SDK re-fetches through authProvider up to 2 times, then enters a terminal state and waits for you to reconnect explicitly |
A 401 right after a refresh means revocation or misconfiguration, not jitter |
On the platform side, only one code signals that a token is dead: unauthorized (401). When the session service receives it, it evicts the cached token, re-mints and resends once within the same request; a client SDK that sees it runs the 2-attempt convergence described above. Other codes (403, 429, 5xx) do not mean the token is bad — do not re-mint because of them.
Quick self-check
Section titled “Quick self-check”tansr doctor: did the credential land in the OS keychain or a plaintext file? Does thelocalProviderssection match what you expect (platform mode / escape hatch)? Any unattended-baseline notice?grepyour repository once forTANSR_APP_KEY/sk-/tansr_sk_to confirm no real value was committed.- When the session service binds
0.0.0.0, is/metricsexposed to the internal network only? Is thestoredirectory a local disk, not shared? - Does your token mint endpoint mint only after your own login, and does it always take
endUserIdfrom the server-side session rather than trusting the request body? Isx-tansr-end-userwritten only by your gateway? - Is a
--v2deployment still running with--legacy-v1/TANSR_SERVE_LEGACY_V1? Aserver.deprecated_optionevent in the logs means it should be scheduled for removal (gone in 1.0.0); move approval submission toPOST /api/approvals/:id/credential. - Is
--host-module-sha256produced by the deployment pipeline together with the.cjsartifact, rather than computed by hand at rollout? - Are lifecycle rules and encryption configured on the cold-tier object storage?
Was this page helpful?
Thanks for your feedback.