- Docs
- Policies
Security and privacy
Cobalt Cockpit runs as a Claude Code mod with your user privileges. It may observe tool calls, file paths, agent events/descriptions, requested and observed model IDs, token/context telemetry, Git state, verification results and submitted task text. The dashboard makes no model calls or independent network requests.
Local persistence#
The host's plugin store holds sound/HUD preferences, up to eight bounded Run Ledgers, release-review records (commit ids), the Stop-hook verification guard's check records (outcome, exit code, a short fingerprint hash and up to six lines of the enrolled script's output, passed through the credential patterns) and script enrollments (project path, script path, sha-256 of the script). Ledgers include identifiers, tool/path metadata, counts, timings, model usage, sanitized NobodyWho receipts, successful bounded edit/write snapshots, deterministic checkpoints and, with guardAlerts on, alert outcomes. Checkpoints can contain explicit task goals, milestone labels and blockers; treat the store as private. Task and HUD state also survive hot reload within the session. Cockpit does not control Claude Code's own transcript retention.
With telemetry on (the default) Cockpit also writes outside the store: a local model-telemetry directory of its own, $XDG_STATE_HOME/cobalt-cockpit/telemetry/ (default ~/.local/state/cobalt-cockpit/telemetry/), holding per-process JSON-lines segments whose fields are enumerations, booleans and numbers only (no prompt, code, path, command, session id, model id or secret), 64 KiB per segment, 4 MiB and 13 months per directory. Cockpit trims that directory itself, best effort, with rm -f on exact telemetry file names when a process first writes. It creates the directory with mkdir -p -m 700, sets each file to 0600 with chmod after the host has written it, reads both modes back with stat and reports them as verified only then; the host's write API has no mode argument and an existing directory keeps its mode, so the modes are requested and checked, not guaranteed. These two are the only places Cockpit writes: no project file, no settings file. Retention and deletion for both are in PRIVACY.md.
Replay omits known sensitive filenames, obvious credential patterns, unavailable files and content exceeding its size limits. It is a heuristic, not a universal secret detector. Hidden reasoning is never stored by Cockpit. Raw tool commands are not ledger fields. NobodyWho ledger access is read-only: prompt/state/details fields are discarded, caller filtering is applied and historical receipts do not count as current activity. NobodyWho is optional; missing files fail silently.
The alert history (v0.5.7 to v0.6.0) is kept in plugin-store keys too: one per alert occurrence (alert-occ:<project hash>:<id>), one per acknowledgment (alert-ack:…), a look marker per project (alert-seen:…) and the digest marker (alert-digest), all bounded and holding a closed vocabulary, numbers, hashes and ids. /cockpit alerts legacy delete <code> is the only command that deletes alert keys on request: it is typed by a person, confirms a dry run's code, and deletes only a v0.5.7/v0.5.8 history key every occurrence of which already has a key of its own.
/cockpit agents and /cockpit ready (v0.6.0) read the swarm and the task and change nothing; they cannot admit, cancel or verify a task, lift a hold or clear a review.
An alert label (v0.6.3, alert-label:<project hash>:<id>, a closed label and a time) is set or removed only by human authority: the terminal's own Enter or the Remote Control bridge. sdk turns and every other origin are refused before anything is read or written. /cockpit alerts review and /cockpit alerts calibration send nothing and change no setting, gate, hold or enforcement.
Export and sharing#
/cockpit alerts export (typed by a person) prints this project's alert occurrences as schema-versioned JSON in a closed vocabulary: ids, levels, kinds, task numbers, times, the host's channel name, and separate notification and acknowledgment fields; no key, path or free text. /ledger export json removes replay bodies, agent descriptions and checkpoint prose, then strips control sequences and detected secrets from string values. Paths, branch names, identifiers and repository metadata may remain sensitive. Review every export and screenshot before sharing. Secrets should never be exported. Do not put credentials in milestone labels, file names or configuration prose.
The plugin declares no MCP server, downloads no runtime code and changes no authentication environment variables. Authentication diagnostics retain only credential kind and known configuration source names, never credential values.
Environment and process access#
Cockpit reads environment variables by name and never changes one of its own process. Eleven of the names are its own configuration, runtime and diagnostics: CLAUDE_CODE_EFFORT_LEVEL (to name where the main loop's reasoning level came from), XDG_STATE_HOME and HOME (to resolve the optional decision-router ledger path and its own telemetry directory), COBALT_REDUCED_MOTION (to freeze animation), COBALT_TELEMETRY_SEGMENT_BYTES (a test knob that can lower, never raise, the telemetry segment size), CLAUDE_CONFIG_DIR (to find Claude Code's installed_plugins.json for /cockpit doctor), and FORCE_PROMPT_CACHING_5M, CLAUDE_CODE_SUBAGENT_PROMPT_CACHE_TTL and ENABLE_PROMPT_CACHING_1H (read by /cockpit doctor to report the subagent prompt-cache policy, reduced to the recognised values on the spot), and SSH_CONNECTION and SSH_TTY (read by /cockpit guard and /cockpit doctor only to tell whether this process was started in an SSH session; each is reduced on the spot to set or not set, and no address, port or device name is kept, shown or sent). Eight are authentication variables, read only to decide whether the session is on API-style authentication, for the subscriptionOnly refusal and the /cockpit auth diagnostic: ANTHROPIC_API_KEY, ANTHROPIC_AUTH_TOKEN, ANTHROPIC_BASE_URL, ANTHROPIC_CUSTOM_HEADERS, CLAUDE_CODE_USE_BEDROCK, CLAUDE_CODE_USE_VERTEX, CLAUDE_CODE_USE_FOUNDRY and CLAUDE_CODE_USE_MANTLE. Each value is reduced on the spot by value !== undefined && value !== ''; only the literal variable name reaches plugin state, and nothing is logged, exported or sent. $.session.authorize() answers the engine's own credential kind; the handle it returns is dropped immediately. Three variables are set for a child process and none for Cockpit's own: DECISION_ROUTER_CONFIG_DIR for the router in JEV mode (below), LC_ALL=C for the stat calls whose error wording the fingerprint reads and for the limit probes, and MALLOC_ARENA_MAX=2 for every measurement process (below).
Cockpit runs local processes with the user's privileges: realpath -m (orchestration only: a claimed path, and each path a scoped helper's Git inspection names) and realpath -e -- <directory> (canonical Git root/session identity); git rev-parse --verify --quiet <name>^{commit} (orchestration only: each revision a scoped helper's Git inspection names); tail -c (a ledger tail larger than 512 KiB); read-only git commands, all with --no-optional-locks: status --porcelain=v2 --branch and rev-parse --show-toplevel for the HUD, rev-parse --show-prefix, config --null --get-regexp for effective clean/process filter commands and check-attr -z --stdin filter for tracked paths (active external filters make candidate/fingerprint UNKNOWN before status/diff), rev-parse --verify HEAD and status --porcelain --untracked-files=all --ignore-submodules=none to identify the commit a release review is tied to, and, for the working-tree fingerprint, diff -O/dev/null --name-only, ls-files -o, ls-files -o -i, ls-files -v, ls-tree -r -z --full-tree, rev-parse --git-path (for info/exclude, info/attributes and objects/info/alternates), config --type=path --get core.excludesFile|core.attributesFile and hash-object --no-filters for raw bytes of regular files and symlink targets, and hash-object with native EOL/encoding normalization only after external filters are ruled out, with readlink -z (plain readlink fallback) for symbolic-link targets, committed or not, stat (GNU --printf '%f %s %i %.9Y %.9Z %n\0', BSD -f as the fallback, -L for Git's excludes, attributes and alternates files and for watched paths) for the kind, size and identity of each path before anything is read, and find -H <dir> ( -type f -o -type l ) -print0 and stat (GNU --printf, BSD -f as the fallback, and stat -- <dir> under LC_ALL=C) over ignored, embedded and watched entries; each of these starts through the measurement limit script (below); the telemetry directory's own maintenance, mkdir -p -m 700, chmod 600, stat -c %a and rm -f -- <file>, each on a path inside that directory and, for rm, on exact telemetry file names only; the audio cues through the first player that answers; only while a session router is selected with /cockpit router (off in every new session), decision ask --caller cockpit --mode local|jev --json <question>: ten requests at the start of a new task the rules do not settle, and one when the router is selected (the local decision router; 5-second timeout for the local provider, 3 for JEV, which is also the most a reading can delay a prompt; its stdout is parsed for a receipt, a yes or no, and names, and nothing else). --mode local is the router's local provider alone. --mode jev is its TypeSafe JEV provider: an HTTPS request made by that program, not by the plugin, under a key the router reads from its own key file. In JEV mode alone, an operator-set routerConfigDir is passed to that child as DECISION_ROUTER_CONFIG_DIR; the plugin never creates, reads or edits router configuration, never sees the key, and refuses a routerConfigDir that resolves inside the project (a repository must not be able to choose the router's endpoint or key file). The router is switched only by a command the person typed, and is reset when a session ends. Finally, only with stopGuard: ENFORCE and a script the person enrolled with /cockpit guard trust, the Stop hook runs that script: an absolute path as a one-element argument array, no shell, the project directory as its working directory, the host's own environment, a timeout of verifyTimeoutSec (default 120 s, at most 600), the script's bytes re-hashed against the enrollment before and after each run; its exit code and the last six lines of its output, passed through the credential patterns, are recorded in the Run Ledger and the store. What the script itself starts is outside Cockpit's control. Internal process invocations are argument arrays. There are two sh -c scripts, both fixed literals: the terminal bell, and (v0.6.4) the measurement limit script, which takes the command as separate arguments after its $0 (cockpit-bounded) and execs them, so no word of a command is ever part of a script; with it come four fixed probes run once per load (echo set under the limits, timeout --version, and a single 64 MiB dd from /dev/zero to /dev/null without and with a 32 MiB cap). The script runs each command under GNU timeout -s KILL where present, which ends the command and every child still in its process group (a descendant that creates its own session or process group escapes it). An already allowed read-only helper Git Bash call is reconstructed from parsed literal arguments with safe flags and quoting, after root/filter preflight; Cockpit starts no new Bash call. The Stop hook runs only the enrolled script by path. There is no runtime MCP server command or package launcher.
The plugin makes no network request of its own: no $.http.fetch, no model call, no MCP call, no dynamic import, no eval. The one program it starts that can reach the network is the decision router in JEV mode, described above, and only in a session where the operator selected it. Its prompt.compose and prompt.submit hooks contribute text to the model request Claude Code already sends, which is the only route by which observed state can reach a model.
Working-tree measurement limits#
The working-tree fingerprint, the HUD's Git line, the release check and the preflight of a helper's Git inspection read the repository with Git and stat, and a repository decides much of what that reads. Since v0.6.4 each of those processes runs under limits Cockpit sets, and what cannot be read safely is UNKNOWN, never clean (docs/implementation-v0.6.4.md):
- Kernel limits. A fixed
sh -cscript that only ever lowers a limit sets, for each process, the virtual address space to 1 GiB wherever the shell accepts it, CPU time to 20 s, the largest file a process may write to 64 MiB and core dumps to 0; the host's timeout (6 s for each Git call) still applies. Git also runs withcore.hooksPath=/dev/null,diff.orderFile=/dev/null, bounded pack windows and caches, streaming for files over 16 MiB and no index threads, underMALLOC_ARENA_MAX=2. Whether the platform enforces the memory limit is probed once per load (again 30 s after a probe that did not answer) and shown by/cockpit doctor. - Typed inspection. Before a path is read it is inspected with
stat, without following its last link. Only regular files are read: a symbolic link is measured by its text and a FIFO, socket or device by its kind alone, never opened. Git's excludes, attributes and alternates files must be ordinary files of at most 1 MiB before Git runs. - Bounds. 1 GiB for one file and 8 GiB in all per fingerprint, 20,000 changed or untracked and 100,000 tracked, ignored or watched files, 4 MiB of output per process and 90 s per fingerprint. A file that changes between its inspection and its read is not believed.
- Termination and concurrency. Each measurement runs under GNU
timeout -s KILLa second inside the host's timeout, so the process and every child still in its process group end together; a descendant that creates a new session or process group leaves the group and is not killed by it. Without a shell that can set the limits, nothing is started. At most two top-level measurement processes run at once in each Claude Code process. - No aggregate memory ceiling. The address-space limit is per process: every child a measurement starts inherits a separate 1 GiB limit, and nothing adds them together. The two slots limit how many measurements run at once; they do not cap resident memory, a measurement's process tree, or all measurements together at 2 GiB (the earlier heading here, "Termination and ceilings", suggested they did). A strict aggregate ceiling needs operating-system enforcement that Cockpit does not set up, such as a cgroup v2 with
memory.max(for example, a systemd scope withMemoryMax=around Claude Code). Details: docs/implementation-v0.6.4.md. - Failure is UNKNOWN with its reason. A goal check or a passing check observed on a measured tree is not kept once the tree can no longer be measured. In a Git repository that cannot be measured safely, a goal check is recorded as UNKNOWN, never ALIGNED; only outside every Git repository (Git says so and no
.gitexists in the directory or a parent) is ALIGNED accepted without a fingerprint, marked UNPROTECTED. A measurement cut short only because a file changed or vanished while it was read is taken once more; a second failure stands. - A stamp a chosen change cannot match. The fingerprint's content digest is SHA-256, cut to 128 bits (two 32-bit FNV hashes before v0.6.4, which chosen file names could make collide).
What this does not cover: an aggregate memory ceiling (above); where the address-space cap is not enforced (/cockpit doctor says so; macOS is not expected to enforce it, not verified), a file Git reads on its own (a linked worktree's commondir, a configuration include, grafts, sparse-checkout, the alternates of an alternate) and a file swapped for a device between its inspection and its read are bounded only by CPU time and the timeout. Each needs write access to the repository's .git directory or the working tree while Cockpit measures. A FIFO Git opens itself still holds each call for the timeout, after which the tree is UNKNOWN. Without GNU timeout (/cockpit doctor says so), a child a blocked measurement started may outlive it. Git commands that Claude Code runs itself, and the commands a model or helper runs through Bash, are not bounded by Cockpit.
Hooks that refuse#
A hook of Cockpit's that fails is not allowed to let an action through. The engine skips a hook that throws, times out or answers a wrong shape and runs what is beneath it, so every hook that can answer or refuse carries a .catch handler that answers in its place — the tool guard, the agent-offer and agent-spawn guards, the /config row guard, the /model and /advisor guards, the checkpoint commands and /cockpit itself; turn.step is the one exception, below. Each handler judges the event it was given with the hook's own rule, so a failure refuses what the hook would have refused and lets everything else run, and a prompt is dropped only where this plugin's own policy would drop it. claude plugin validate --strict --json reports each hook and whether it has a handler.
turn.step is the exception, and the reason is structural: for a streaming event the handler must itself be a generator. Its pre-decision readings are wrapped instead, so the hook reaches its Fable, subscription and strict-advisor decisions even when a reading beneath it does not answer. Hooks that only observe — /init, /effort, /login, /logout, post-tool readings — forward, because refusing a person's own action over a lost reading is not a guard.
Inside the tool guard the same rule holds one level down: the attribution, instruction-file and destructive-command guards each run under failClosed, and a guard that throws refuses the call it guards, naming the guard, the tool and an allow-listed kind of error and nothing of the command, a path or the error's message. Nothing is retried or approved on the caller's behalf, tools the guard does not cover keep working, and the next call is checked afresh; the operator's own options (blastRadiusGuard, attributionGuard) still decide which guards run. A check that cannot be made is not an answer: an unreadable filesystem does not mean "the instruction file exists", and under subscriptionOnly only a confirmed subscription sign-in passes: an API-style configuration, an unconfirmed sign-in (unverified), a state not yet read and a state that cannot be read are each refused.
Cockpit has no hook on Claude Code's permission check (tool.check) and asks no permission decision of its own. Its guards run earlier, in tool.call, and only ever refuse a call or put a question to you; nothing in the plugin approves one, so your permission rules, your permission mode and the engine's own dialog decide every call that Cockpit lets through.
Helpers and their reports#
With orchestration on, a bound helper may use two host control tools whatever its ownership: tool discovery (ToolSearch) and the report hand-back to its parent (SubagentHandback). Neither has a resource effect. Discovery grants no right to run what it finds: every tool still passes its own ownership guard and Claude Code's permission check, so a read-only helper's write, shell command and delegation are refused exactly as before. A hand-back records only the calling helper's own report. How a report arrived (reported, host_accepted, answer_observed, unavailable) is recorded on the task and is never verification; only the commander verifies.
Read-only Git inspection by helpers#
With orchestration on, a helper's shell is refused unless its task owns *, with one exception: read-only Git inspection. Two rules apply, by what the task owns.
Named paths. One plain git diff|show|log|status|grep|ls-files command, accepted only when all of this holds: no pipe, chain, redirect, substitution, variable, control character, wrapper, assignment or global Git option; every option is on that subcommand's fixed list (display and search options, matched exactly, with no abbreviation and no bundle that holds anything else; nothing that names another file, an output, a format, a pager or a driver); every revision is a plain name that git rev-parse --verify <name>^{commit} proves to be a commit, so none addresses a tree or a file; an explicit -- is followed by literal paths (no wildcard, pathspec magic or ~), each canonicalized with realpath -m and inside the task's owned resources; git show <revision>:<path> only for a path that is owned both as written and as realpath resolves it, because Git reads it by name in the commit's tree and follows no link; the repository is the session's own and no clean or process filter is active on it. A path outside the scope is refused however it is spelled, and the refusal names the scope.
*. The wider refusal list over diff, status, log, show, ls-files, rev-parse and grep, with the -- separator accepted and any word after it that begins with a dash refused. The list follows how Git reads options, not how they are usually typed: a long option is refused when its name is a prefix of a refused one or begins with one (Git accepts unambiguous abbreviations, so --op=<command> is --open-files-in-pager=<command>), and any one-dash word containing O is refused (Git bundles short options, so -nO<command> is -n -O<command>). v0.6.0 and earlier tested only how a word begins and could be made to run a program that way; see docs/implementation-v0.6.1.md.
In both cases the text the agent typed is never executed: the command is written again from its parsed words, each in single quotes, behind --no-pager, --no-optional-locks, --no-replace-objects, --literal-pathspecs, --no-ext-diff and --no-textconv, with repository hooks off and the diff order file overridden (v0.6.4), and a scoped command is pinned with -C to the verified directory and given canonical absolute paths. The hook refuses or passes the call on; it never approves one, so Claude Code's permission rules and dialog still decide whether git runs.
The commander's own inspection is judged by the * list as well, to decide whether the command is held while a helper owns the repository. It runs as typed, so nothing quoted in it is taken on trust and nothing bash would change is accepted: no $, <, > or backtick anywhere, no control character, no brace at all other than a revision's own (HEAD@{1}, HEAD^{tree}; bash decides brace expansion before it removes quotes, so a quoted or escaped brace is no safer), and no filename pattern in an option or at the start of a word (where it could expand to a file whose name begins with a dash).
What a command cannot do, and what an assignment can. A read-only Git command never widens a scope, and no reviewer is given * because it asked to inspect Git. The commander's assignment is what sets a scope, and one thing about it is older than this release and unchanged: an owned_resources entry that is a pattern, starts with ~ or does not resolve is taken as *. The assign reply now says when that happened; a reader meant to be narrow must be given literal paths.
What a scoped inspection can show that is not path-scoped: the hash, author, date and message of the commits it lists. What it cannot: another file's content or name. What a * reader can do that this rule does not stop: make Git compare two files outside the repository (Git's implicit no-index diff), which gains nothing over Read, and, in a partial clone, cause a fetch of missing objects from the promisor remote. The rules, the option lists and the tests: docs/implementation-v0.6.1.md.
Agent identity and the architect role#
The architect role requires the exact agent type cobalt-cockpit:architect. At spawn, the host must attribute its definition to provider.plugin: cobalt-cockpit. Another project's or plugin's architect suffix does not establish this identity. Other role labels still follow their fixed type-name mapping.
An admitted Sonnet-led Opus consultation binds its [task:ID] and host agent id, forces Opus and a read-only assignment, and records the host-observed answer. The host supplies only the provider's plugin name, not a signed source identity: another installation also named cobalt-cockpit is not distinguished. Keep loaded plugin sources under your control. Publication remains an independent human decision. Read-only helper Git inspections are reconstructed with the hardened flags and external-filter/root preflight; this reduces repository-defined execution but remains a change/ownership guard rather than a process sandbox.
Guard limitations#
Command safety and repository hygiene use recognizable syntax and user confirmation. They are not a sandbox or an exhaustive security boundary. Verification records observations and user/agent-reported evidence; it cannot guarantee code correctness. Keep Claude Code permission controls enabled.
Owner alerts are advisory. Quiet hours and guardNotifyKinds are your decision not to be notified: an alert that is deferred or whose kind is off is shown only on the HUD and by /cockpit guard until the window ends, and nobody is told if no one is looking. A critical (security-failure) alert is deferred only if you set guardQuietCritical to defer. None of these settings changes a gate, a hold, a review or a release approval, and they are accepted only from you in the config menu, never from a plugin or a model. Quiet hours trust the machine's clock and time-zone data; a zone or window that cannot be read turns them off rather than holding an alert back.
Reporting a vulnerability#
Use GitHub's private vulnerability reporting for this repository if available. Otherwise contact the repository maintainer through their GitHub profile to arrange a private channel. Do not post secrets, private ledgers or exploit details in a public issue. Include affected version, a minimal synthetic reproduction and expected impact.
Secret scanning#
Every pull request and push to main runs the Secret scan workflow (Gitleaks, version and checksum pinned, read-only token, actions pinned to a commit). It scans all reachable history and the working tree and prints only rule, file, line and commit, never a value. Maintainers should mark its Gitleaks job as a required status check in the branch protection for main.
To run the same check locally (Linux x86-64; the script downloads the pinned release and verifies its SHA-256):
scripts/scan-secrets.sh # reports go to a private temp directory (a private temporary directory by default, mode 0600): do not publish them
python3 scripts/audit-public.py # portability and credential-shape audit, including AWS AKIA/ASIA key IDs.gitleaksignore lists exact fingerprints of reviewed synthetic fixtures in already-public history; nothing wider is excluded. Test fixtures that look like credentials are assembled from parts at run time. If a scan reports a real credential, revoke it at the provider first: deleting the line does not invalidate it.