# Native sessions with host authentication **Experimental, on by default for Codex/Claude, macOS and Linux.** Node bridges the installed Codex or Claude Code client. Authenticate/configure that client once as the OS user that runs Node. `SESSION_ISOLATION=native` does not require another login or a new API key. Node does not parse, copy, refresh or proxy subscription tokens. ## Boundary The official client remains a trusted host process and owns authentication. Its model-controlled tools are restricted separately: - Codex uses a named filesystem permission profile, a clean tool environment, and its native OS sandbox. Legacy thread/start and thread/resume sandbox overrides are omitted so they cannot replace that profile. The in-process `view_image` tool and the host-configured `notify` command are explicitly disabled, including when the user's configuration enables them. - Claude retains its normal authentication home and uses `--restricted`, a strict MCP configuration and an explicit tool set. Without selected MCP tools it also uses `--safe-mode`. With Core-selected tools, it uses explicit customization controls instead: user/project/local settings are ignored, hooks and skills are disabled, automatic memory is off and ambient instruction files are excluded. Only the supplied MCP configuration is loaded. Neither path uses `--bare`, so the official client can retain its OAuth/Keychain login. Managed policy still applies. Node never copies login tokens into MCP configuration. **Bash is the only default local tool.** Read/Edit/Write/Glob/Grep require an explicit operator opt-in; they use client permission checks, not an OS sandbox. Bash uses the built-in OS sandbox with `failIfUnavailable=true`, no excluded commands and no unsandboxed retry. Credentials are denied to command environments. macOS also enables global subprocess scrubbing; Linux uses the normal Bash sandbox's credential deny rules and PID/proc namespaces. The global scrub switch in Claude 2.1.259 adds broad Linux writable roots and must not be enabled here. `CLAUDE_CODE_TMPDIR` also selects the Run's private temporary directory for Claude's command bookkeeping, rather than the shared `/tmp/claude-` path. - Authentication directories, other sessions and Node control files are outside tool grants. Shells receive no platform token or ambient loader configuration. Codex tool HOME is private; Claude keeps its auth home in the trusted client and denies it to tools. A network filter is not used as a substitute for preventing readable credentials from returning in Run output. - Host plugins, ambient MCP servers, hooks and cross-session/desktop/browser tools are not part of this mode. Owner-selected MCP tools arrive through Core's trusted snapshot and per-call authorization. Node does not implement another model loop. The official binary, OS user/admin, client settings delivered by an administrator and allowed code/library paths are trusted. This is not isolation from a hostile host process, compromised official client or administrator policy that weakens its sandbox. It is not a blanket claim that arbitrary client versions are safe. Do not expose an operator's personal subscription as a shared public service. Account authorization, provider terms, spending limits and access control remain separate requirements. Use with trusted callers while this feature is experimental. ## Session lifetime One Node serves multiple conversations. Each active Run starts its client process; its tools execute in that session's sandbox. Idle sessions retain data without keeping client processes alive. Core's trusted principal, Agent, conversation key and Node's Core namespace select the storage scope. Payload fields cannot select a native session. A cross-process lock prevents overlapping writes to the same conversation; cancellation of A does not stop B. A → B → A resumes A's native session. Host personal transcripts are not imported as conversation history. Codex controls the resumed thread ID; Claude additionally partitions its project transcripts using `CLAUDE_CODE_PROJECT_DIR_NAME`, while keeping `CLAUDE_CONFIG_DIR` unchanged for authentication. Native mode defaults to `HOST_AUTH_CONCURRENCY=serial`: one participating Node client per provider and shared host HOME at a time. The lock lives at `$HOME/.local/state/openlinker-agent-node/host-auth-.lock`, using the trusted client's HOME, not the session tool HOME, TMPDIR or XDG_STATE_HOME. State is owner-only (0700 directory, 0600 lock); existing directories are never chmodded. HOME aliases are canonicalized. Unsafe ownership, writable parents or symlinks beneath HOME fail closed, with no fallback to `/tmp`. Tools cannot read, write or unlink this reserved state; do not place session storage there. Multiple Node processes/Agents/session roots coordinate **only when they share the underlying HOME state directory and working OS file locks**. Private `/tmp` namespaces do not split this lock. Different container HOME mounts, different HOME values or isolated state copies still do, even if paths or client credentials match. Node cannot infer or detect another namespace's mount or shared account. This is a host-local file lock, not distributed coordination. The lock covers client startup, exit, retries and session persistence. Waiting is cancellable, counts toward the Run timeout, uses an assigned capacity slot and emits one status event. For serial mode, explicitly set `OPENLINKER_AGENT_NODE_CAPACITY=1` so this Node does not reserve extra Runs that could be dispatched elsewhere. Node does not silently change configured capacity. Waiters retry every 50ms; this is not FIFO. Sustained contention may cause a waiter to reach its timeout without running; there is no fairness guarantee. `HOST_AUTH_CONCURRENCY=client-managed` explicitly permits concurrent clients. Use it only for workloads whose client/authentication concurrency is independently verified; it does not add token-refresh protection. Ordinary mode is unchanged. This is **cooperative Node admission, not an account-wide refresh lock**. Older Nodes, opt-out Nodes and separately launched terminal/desktop clients do not participate. Avoid sharing a login with those clients during account-sensitive evaluation. Normal cancellation waits for client shutdown before releasing the lock. A hard-killed Node can leave an orphan client after its lock is released; check and stop that Node's remaining client processes before restarting it. Do not delete the HOME lock while any participating Node is running: the file is retained intentionally, and the OS releases ownership. When upgrading from the earlier `/tmp` lock candidate, stop all old participating Nodes and settle or stop their client processes before starting the new version. The two lock locations do not coordinate; no old lock/session data is deleted automatically. Real OAuth rotation remains untested; see the [open refresh-concurrency item](native-session-isolation-follow-ups.md). ## Setup Install supported official clients (tested baseline: Codex 0.153.0 and Claude Code 2.1.259). Configure/login to the selected client normally **as the Node OS user**. Linux needs functioning unprivileged user/network/PID namespaces; Claude also needs `bwrap`, `socat` and `rg`. macOS uses the client's Seatbelt sandbox. A separate npm installation of SRT is no longer required for Node's native mode. Authentication/session directories must be outside system/code read roots. On Linux Codex needs its per-process helper-alias directory and executable; Node verifies that the directory contains only known aliases and an empty lock before granting read access. It does not grant the authentication home or tmp tree. Ubuntu 26.04's `apparmor` package enforces a `bwrap-userns-restrict` profile for `/usr/bin/bwrap`: bwrap can create namespaces, but every process it starts runs without capabilities, including in further user namespaces. Under this default, Codex native isolation works and Node's startup probe passes, but Claude's sandbox cannot start its seccomp helper, which needs a capability-bearing nested user namespace. Every Claude Bash call then fails closed with `apply-seccomp: write /proc/self/setgroups (nested userns is capability-restricted ...)`; file tools stay in the workspace and nothing falls back to an unsandboxed shell. Granting bwrap's children capability-bearing user namespaces is exactly what this distro policy prevents, so it is a host security decision outside Node. The native-isolation CI job runs on Ubuntu 24.04, not 26.04. Do not load a second AppArmor profile that attaches `/usr/bin/bwrap` alongside the distro one: the kernel then applies neither (`conflicting profile attachments` in the audit log), bwrap fails with `loopback: Failed RTM_NEWADDR`, and Node's startup probe rejects native isolation for both clients. ```sh export OPENLINKER_AGENT_NODE_ADAPTER=codex # or claude; native isolation is the default export OPENLINKER_AGENT_NODE_CAPACITY=1 # recommended with default serial admission # Optional: defaults to $HOME/.local/state/openlinker-agent-node-sessions export OPENLINKER_AGENT_NODE_SESSION_ROOT=/absolute/private/node-sessions # Keep your existing HOME and, if configured, CODEX_HOME / CLAUDE_CONFIG_DIR. # No additional CODEX_API_KEY or ANTHROPIC_API_KEY is required for cached login. ``` `SESSION_ROOT` must be owner-only, have no unsafe writable ancestors and lie outside every git worktree (no `.git` directory or file in it or any ancestor). The trusted client reads its working directory's git status, recent commits and project instructions before any tool sandbox applies, so a session under a repository would expose that repository to the model. Do not run Node as root. Native mode runs each conversation in a private workspace, so it rejects `CODEX_WORKSPACE` / `CLAUDE_WORKSPACE`, `*_SESSION_REUSE=false`, `SESSION_STORE`, delegation and `ENV_ALLOWLIST` instead of ignoring them. `*_SESSION_REUSE` defaults to `true` in native mode. There is no automatic unsandboxed fallback: on unsupported platforms, as root, or with those settings, startup fails until you fix the setting or set `SESSION_ISOLATION=off` explicitly. A Codex mock response starts no client and keeps the `off` default; HTTP, A2A and command adapters are never isolated. Options use the `OPENLINKER_AGENT_NODE_` prefix: | Option | Meaning | | --- | --- | | `SESSION_ISOLATION` | `native` (default for Codex/Claude) or `off` | | `HOST_AUTH_CONCURRENCY` | Native only: unset/`serial` serializes Node clients per shared host HOME/provider; `client-managed` explicitly allows concurrent clients | | `SESSION_ROOT` | Private persistent storage outside any git worktree; default `$HOME/.local/state/openlinker-agent-node-sessions` | | `SESSION_TEMP_ROOT` | Optional private, short temporary root (resolved path ≤40 bytes) | | `SESSION_READ_PATHS` | JSON array of additional read-only code/library paths for shell tools; may not expose auth or session/control directories | | `SESSION_NETWORK_DOMAINS` | JSON array of exact public HTTPS hostnames for sandboxed command networking; empty means offline commands | | `CLAUDE_ALLOWED_TOOLS` | Unset/empty defaults to Bash. Explicit JSON list may opt trusted workloads into Read, Edit, Write, Glob, Grep, Bash; see the file-tool boundary below | | `CODEX_WEB_SEARCH` / `CLAUDE_WEB_SEARCH` | Controls the client's native search tool separately | ### Custom model gateways Node no longer overwrites the default Codex model endpoint. Codex retains its existing model-provider/auth configuration. Claude retains the configured `ANTHROPIC_BASE_URL`/standard credential environment or cached login. Explicit `CODEX_BASE_URL` and `CLAUDE_BASE_URL` overrides remain supported (HTTPS port 443, complete API base path). An explicit Codex gateway uses `CODEX_API_KEY`, as before; that optional path does not make a key mandatory for normal cached login. `SESSION_NETWORK_DOMAINS` restricts **sandboxed command** networking, not the trusted client's model API/auth traffic or its server-side WebSearch. Adding a model gateway does not automatically grant tools access to it. TLS verification is never disabled. Arbitrary environment forwarding remains rejected. Claude's restricted mode ignores user/project customization settings; setups depending on a settings-only helper or custom/cloud authentication need separate validation, not automatic token extraction or a subscription proxy. ## Migration and verification ### File-tool opt-in boundary Claude's application-level file tools are not equivalent to its sandboxed Bash tool. Keep the default for account-sensitive workloads; shell commands can read, search and edit the session's own files. `CLAUDE_WEB_SEARCH` remains separately controlled (on by default, explicit `false` turns it off) and does not follow this default. Do not put hard links to sensitive host files into a session workspace. A hard link is another name for the same inode, not a path that can be checked with `realpath`. The real-client suite separately checks links created by sandboxed Bash and hard links preseeded by the trusted test host. The latter can be read by explicitly enabled Read/Grep and by sandboxed Bash itself. A Bash-only tool set does not revoke an inode already granted by the host. A successful symlink test does not prove protection against arbitrary preexisting inode aliases. Turning tools off does not erase data already in transcripts or Core history. Do not reuse a conversation that may already contain credentials after narrowing its tool permissions. The opt-in matrix covers the five file tools, file/directory symlinks, hard links, and Linux `/proc/self` aliases. Positive controls verify real file-tool execution; host-content checks distinguish atomic replacement of an alias from writes to the original host inode. The default case also submits unadvertised file-tool calls and requires rejection. Results apply only to the pinned clients and tested cases; they are not a proof against symlink-swap races or every client file/IPC API. ### Existing SRT sessions The old whole-client SRT design required dedicated keys and changed authentication homes. The new host-auth scope is deliberately separate: old workspaces, native history and potentially exposed credentials are neither copied nor deleted. Resume starts fresh once when migrating, then persists in the new scope. This applies to migration from the old whole-client SRT mode to host-auth mode; the later Bash-default/view-image hardening does not change the scope again. Retained Core history is separate from the old client's private session data. Remove `SESSION_SANDBOX_BIN`; Node rejects this obsolete override instead of silently ignoring it. Legacy `SESSION_STORE`, delegation sockets and arbitrary `ENV_ALLOWLIST` are not combined with native isolation. Startup verifies CLI capabilities and OS prerequisites. On macOS Codex also runs a real allowed-write/denied-read sandbox probe; Linux probes user/network/PID namespace availability. Claude itself must initialize its sandbox before a task, with `failIfUnavailable`; a help/version probe alone is not evidence of file enforcement. Failures never select an unsandboxed fallback. Run `scripts/test-native-session-isolation.sh` with both official client paths. The acceptance suite uses private synthetic auth homes and local mock model responses to exercise actual tools, cached auth, A-B-A recovery, credential-file reads, the separate file-tool link matrix, cross-session reads, explicitly opted-in scoped file edits, outside writes, environment and parent-process probes. macOS additionally checks a synthetic item in an explicitly named temporary keychain, never the login keychain. These tests do not spend subscription/API quota and do not establish real model, WebSearch or account-policy acceptance. The macOS/Linux CI matrix is configured to run this suite on PRs and version-tag pushes; configuration is not a pass result. Current source `25c49b2` passed the GitHub macOS and Ubuntu 24.04 amd64 jobs in [PR #36 CI](https://github.com/OpenLinker-ai/openlinker-agent-node/actions/runs/34810002330), including actual installed-client host-auth, link and admission tests. Local macOS arm64 and an isolated Ubuntu 24.04.5 arm64 OrbStack machine also passed. The latter uses the OrbStack kernel without AppArmor; the hosted Ubuntu result provides separate evidence with its OS policy and executable-specific userns prerequisite. No result claims all target Linux hosts or live-account safety. See the [current acceptance record](host-auth-acceptance.md); older container and whole-client SRT results are not substituted for this candidate. Linux can create an identically named file in an empty private tmpfs overlay. The acceptance check verifies that host credential files/directories remain unchanged; successful writes to disposable overlays do not imply host access. There are still **no per-session disk, memory or process-count hard quotas**. `SESSION_TEMP_ROOT` and timeouts do not provide them. A task can exhaust its filesystem or account budget without reading credentials. See the [follow-up record](native-session-isolation-follow-ups.md). For mTLS deployments, upgrade Core with the certificate-renewal compatibility fix before switching this Node/Host. Schema 099 alone is insufficient; the release notes pin the minimum Core commit. Preserve the existing identity, private key, DataDir and spool. Do not clear local state after a Core database reset; recover the matching server identity through an administrative plan. The minimum Core source for this renewal-compatible release is [`a855ba45b0ec14dc45dc2b0f29899a2f915c8d5a`](https://github.com/OpenLinker-ai/openlinker-core/commit/a855ba45b0ec14dc45dc2b0f29899a2f915c8d5a) (or a descendant containing that fix). Deploy it before upgrading an mTLS Node/Host.