CmdWarden - Product and Architecture Specification¶
Status: Handoff-ready (wayfinder map complete)
Product: CmdWarden
CLI: cw (alias cmdwarden)
Stack: C# / .NET, Windows-native
Map: CmdWarden product handoff (beyond spike)
This document consolidates standing decisions, research, and resolved wayfinder tickets so implementation can proceed without reopening what the product is. Domain language lives in the Glossary. Research notes and prototypes are no longer in the repo. Find them in git history before commit 14e5e6b under docs/research/ and docs/prototypes/.
1. Purpose and positioning¶
CmdWarden is a Windows security layer that gates CLI secret use by tool and Launcher identity so AI harnesses and supply-chain code cannot freely read or exfiltrate credentials.
It is an inspired twin of macOS Automic Vault's job (vault + tool×launcher gate + scan/harden + approvals + audit) - not a CLI clone, not a shared codebase, not multi-OS monorepo with upstream.
First user: this developer machine. First catalog: gh, git, az, docker. Spike vertical: end-to-end gh (PATH shim → Session Agent → identity → vault → Approval Gate → real gh).
2. Standing architecture decisions¶
| Area | Decision |
|---|---|
| Product scope | Full core twin (vault, gate, scan/harden, approvals, audit) - not agent-only |
| Shell | CLI (cw / cmdwarden) + per-user Session Agent |
| Agent host | Per-user interactive session process - not Session 0 Windows Service |
| Vault | Windows Credential Manager, CRED_TYPE_GENERIC, product-prefixed targets |
| Identity | Hybrid L3: Authenticode + process chain preferred; path+SHA-256 fallback; unknown fails closed for auto-approve |
| Harden | PATH shims only - no API hooking / detours |
| IPC | gRPC over Windows named pipes; CurrentUserOnly ACL |
| Approvals | WPF card (out-of-process helper); MessageBox transitional/CI; phone/OOB out of scope; UI detail: Approval Gate |
| Policy levels | Deny / Read / Trusted / Full |
| Defaults | AI Harness → Read; Terminal → Trusted; unknown/unenrolled → Deny |
| Command classes | read / write / secret-reveal / unknown (unknown auto-allows only under Full) |
3. Components¶
┌─────────────┐ named pipe gRPC ┌──────────────────┐
│ cw / shim │ ───────────────────────► │ Session Agent │
│ (client) │ │ - identity │
└─────────────┘ │ - policy │
│ - vault (CredMan)│
│ - approval UI │
│ - audit write │
└──────────────────┘
│
shims early on user PATH │
▼
real tool (absolute path)
| Component | Responsibility |
|---|---|
| CmdWarden.Contracts | Domain types, protos, channel factory, policy keys |
| CmdWarden.Agent | Session Agent host: gRPC, identity, vault, policy load, Approval Gate, audit |
| CmdWarden.Cli | cw / cmdwarden - doctor, vault, policy, agent, audit, scan, harden |
| Shims | Multiplexed or per-tool early PATH stand-ins; ask agent; spawn real tool |
Product root (state): %LOCALAPPDATA%\CmdWarden\
- shims\ - PATH prepend only this dir
- policy.json - enrollment and levels
- audit\ - NDJSON gate log
- Harden pins (absolute real path + hash per tool)
Tool package (dotnet tool) holds binaries only - not durable state.
4. Session Agent and IPC¶
- Listens on named pipe
CmdWarden-<UserName>(overrideCW_PIPE_NAME). - CurrentUserOnly pipe ACL.
- Surfaces: health, caller identity, save/release/delete secret (expand to Authorize for shims as spike continues).
- Lazy start: first
cwor shim call starts agent if down; alsocw agent start|stop|status. - Approval mode: default/
prompt= process helper card;messagebox= transitional MessageBox; tests:CW_APPROVAL_MODE=allow|deny|off.
Research: session-agent-ipc.md.
5. Identity (Launcher)¶
On each request:
- Resolve client PID from named pipe (OS-bound).
- Walk parent chain; populate Authenticode or path+hash per node.
- Select policy launcher (skip agent/cli/dotnet as appropriate).
- Auto-approve eligible only if authenticode or pathhash and not unknown/pid-reuse.
Policy keys: auth:sha1:<thumbprint> | pathhash:sha256:<hex> | unknown.
Research: windows-launcher-identity.md.
6. Policy and enrollment¶
Levels × classes (auto-allow matrix)¶
| Level \ Class | read | write | secret-reveal | unknown |
|---|---|---|---|---|
| Deny | no | no | no | no |
| Read | yes | no | no | no |
| Trusted | yes | yes | no | no |
| Full | yes | yes | yes | yes |
If not auto-allow → Approval Gate (user: Approve Once / Deny; domain: Allow once / Deny). If UI unavailable → fail closed (block).
Enrollment (v1)¶
- Explicit only - no silent auto-enroll.
- CLI:
cw policy enroll --kind terminal|ai-harness[--key],list,set,unenroll,path,sessions. - Bind policy key; store display path for list UX.
- Key change (new hash/thumbprint) → fail closed, re-enroll required.
- Kinds only:
ai_harness(default Read),terminal(default Trusted).
Issue: launcher enrollment UX.
7. Vault¶
- Win32 Cred* GENERIC under targets such as
CmdWarden/secret/<NAME>. - Release only after policy (and optional Approval Gate); audit write first (fail closed if audit fails).
- Short-lived materialization into child process only for inject/shim (never leave secrets in parent env).
- Credential helper gate:
HelperCredential { tool, action, server_url, username, secret }servesdockerregistry credentials fromCmdWarden/docker/<ServerURL>. - The Agent derives the vault name and the class:
getandlistare read;storeanderaseare write. - Chain rule: the pinned, signed real tool sits directly above the helper, or one docker plugin (
docker-compose.exe,docker-buildx.exe) sits between them. Any other chain deniesHelperParentMissing. - The launcher resolver skips a file under
shims/, a pinned tool, and a plugin directly below a pinned tool. A plugin name alone does not count. - A live shim grant (
Authorize) covers helper reads from its chain with reasonRunCovered. It never coversstoreorerase. Policy edits and re-pins clear it. - Every action writes a gate row with the URL as the secret name and no value. Docker entries never appear in the Secrets tab.
- Helper binary:
docker-credential-cmdwarden.exeships inshim-payload/andcw harden dockercopies it toshims/. Protocol: one argv action, stdin payload, protocol JSON on stdout. Not found printscredentials not found in native keychainand exits 1. A deny printsCmdWarden: docker registry credential denied (<reason>)and exits 1.listprints URL to username only. - Strong mode:
cw harden docker --strongreads CredMan forDocker Credentialsentries and inlineauths, saves each toCmdWarden/docker/<ServerURL>if absent or equal, erases the legacy entries, then writesconfig.jsonatomically withcredsStore: cmdwarden. A different vault value or an entry over 2560 bytes fails the whole harden before any change. A failure before the config write puts the originals back and removes the vault copies. The pin document recordsmode: strongand the previouscredsStore; the shim grant then carries noDOCKER_AUTH_CONFIG. Doctor and the Hardened Tools tab report Degraded oncredsStoredrift or a returned legacy entry; re-harden repairs.cw unharden dockerwrites the entries back in wincred layout, deletesCmdWarden/docker/*, restorescredsStore, and removes the pin, shim, and helper. - Strong git:
cw harden git --strongreads CredMan for<namespace>:*entries (credential.namespace, defaultgit), saves each toCmdWarden/git/<key>if absent or equal, erases the legacy entries, then writes the global config throughgit config --global:--replace-all credential.helper ""and--addthe helper path inshform. It fails closed before any write whencredential.credentialStoreisdpapiorplaintextor when~\.git-credentialsexists. The previous globalcredential.helpervalues and every gh host-scoped helper block are saved in the pin document and removed; the system file stays. In strong mode the grant stripsGIT_CONFIG_GLOBAL,GIT_CONFIG_SYSTEM, andGIT_CONFIG_NOSYSTEMfrom the child env, and acredential.*key inGIT_CONFIG_KEY_<n>classifies like-c. Doctor reports Degraded on helper chain drift, a returned gh block, or a returned<namespace>:entry; re-harden repairs.cw unharden gitrestores the config lines and gh blocks, writes the entries back in GCM layout, deletesCmdWarden/git/*, and removes the pin, shim, and helper. - Strong gh store: the vault holds
CmdWarden/gh/<host>for the active slot andCmdWarden/gh/<user>@<host>per user; the active user per host comes fromhosts.yml; entries are hidden from the Secrets tab. When the gh pin saysmode: strongthe Agent readsCmdWarden/gh/*only and ignores the compatGH_TOKEN. Injection by host class:GH_TOKENalways for the github.com slot;GH_ENTERPRISE_TOKENwhen the vault holds one GHES slot, or when argv--hostname,-R host/owner/repo, or envGH_HOSTnames a vaulted GHES slot; two GHES slots and no named host inject none. Each released entry writes its own audit row with the vault name.MigrateToolStore { tool=gh, argv }is served only in strong mode from a live granted run: login and refresh enumerategh:<host>:<user>andoauth_tokenlines, verify each with the realgh auth status --hostname, save-if-absent-or-equal, delete the stock entries, and stripoauth_token; logout drops the vault entrieshosts.ymlno longer lists. The shim calls it when the grant saysmigrate_after_runand the child exits 0. - Strong gh harden:
cw harden gh --strongruns the same migration asMigrateToolStorewith the pinned realgh; a different vault value or a failed verify fails the whole harden before any delete. Nogh auth logout. The pin document recordsmode: strongand the host and user list; the compatGH_TOKENimport is skipped.--tokenwith--strongverifies and writesCmdWarden/gh/<host>for--hostname(defaultgithub.com). Doctor reports Degraded on a returnedgh:<host>:*entry oroauth_tokenline ("stock gh token returned") and on a host whose active user has no vault token ("no vault token for"); re-harden repairs the first two. The Hardened Tools row and cw harden --listshowHardened (strong - N hosts, M accounts in vault).cw unharden ghwrites every entry back in stock layout, writes the active user's token to thegh:<host>:slot, deletesCmdWarden/gh/*, leaves the compatGH_TOKEN, and removes the pin and shim. Residual: realgh auth switch, and a logout that switches the active user, read the stock keyring and fail in strong mode; log in again through the shim instead.
Research: credential-manager-vault.md.
Vault secrets manager UI¶
UI source of truth: CmdWarden Vault.
Summary for architecture readers:
- Chrome: Start-menu WPF window on
net10.0-windows(CmdWarden Vault; the brief targeted WinUI 3, shipped as WPF to match the Approval Gate, see winui-secrets-manager-hosting.md); single-column name list + toolbar (Add / Refresh / Delete). - Host: Separate unpackaged process, gRPC client of the Session Agent over named pipes -- never in-process with the agent or shared with the Approval Gate helper.
- Surface: secret names only; never values. Add modal (name + masked value); overwrite confirm-replace; delete confirm-by-name. Agent-down banner disables actions.
- Agent contract: new
ListSecretNamesRPC (names only viaCredEnumerateWfilter); reusesSaveSecret/DeleteSecret; no Approval Gate on list. - Install: per-user
.lnkshortcut under%APPDATA%\Microsoft\Windows\Start Menu\Programs\. - Non-goals: inject/reveal from UI; Windows Hello; full management app; always-on tray.
8. Approval Gate¶
UI source of truth: Approval Gate.
Summary for architecture readers:
- Chrome: Automic-style Deny / Approve Once card, Windows-native layout. Native caption uses the Lintel product mark via
ApplicationIcon; in-window header is compact mark +CmdWarden(8px gap). - Host: Out-of-process Fluent-style helper under
agent/approval-gate/(default interactive). MessageBox viaCW_APPROVAL_MODE=messageboxonly. Missing helper → Unavailable (fail closed). - Surface: native title bar (Lintel +
CmdWarden); compact in-window brand row; launcher icon + display name +wants to run; command block (invocation, tool path,cwd, secret names); reason line; Details expander (collapsed); Deny / Approve Once. - Never: secret values; Always Approve on dialog; full process chain on main surface.
- Outcomes: Approve Once →
AllowOnce; Deny →Deny; cannot show / timeout →Unavailable→ fail closed. - Allow for session: third button, shown only for enrolled launchers and never for secret-reveal; Approve Once stays the default. Grants the launcher process (pid + start time) the approved class and lower for tool + secret until it exits or is idle 60 minutes (
CW_SESSION_IDLE_SECONDS). Granting call is auditedsession-grant; covered callssession-allowwith reasonSessionAllow. Helper exit code 3; scripted modeCW_APPROVAL_MODE=session. - Timed session grants (#46): the popup offers Until exit, 10 minutes, or 1 hour for Allow for session. Exit code bits 8-9 carry the length (0x100 = 10 minutes, 0x200 = 1 hour); a length on any other answer fails closed. The grant stores an end time. Of two end times the earlier one stays, so a later answer never makes a grant last longer.
cw policy sessionsand the Secret Gates page show the end time. Thesession-grantaudit row hasgrantLength:10m,1h, oruntil-exit. - Deny cooldown: after a Deny, the same launcher process gets no new popup for that tool for 2 minutes (
CW_DENY_COOLDOWN_SECONDS). Other arguments do not open a new popup. Each blocked call is auditeddenywith reasonDenyCooldown. Only one popup shows at a time. A waiting call checks the cooldown before its popup opens. - Approve Once lasts the session: an Approve Once click grants the launcher process the one class it showed for tool + secret, on the same terms as Allow for session (enrolled launcher, never secret-reveal, launcher exit or 60 idle minutes). A higher class prompts again. A later decision adds to the live grant, so a narrow answer never takes coverage away. Covered calls audit
session-allow; the granting call still auditsallow-once. - Real input only (#23): Approve Once and Allow for session accept only keyboard or mouse input that the helper's low-level hooks saw without the injected flag.
SendInput,PostMessage, and UI AutomationInvokeget no answer, and the popup showsUse your keyboard, or move the mouse off this card and back.A click counts only when real mouse moves brought the pointer onto the popup. A pointer that a program moves (SetCursorPos), or that is on the popup when it opens, must leave the popup and come back. Deny accepts every input type. - Approval binds the files (#30):
cw injectresolves the program on PATH (with PATHEXT) and finds the script its interpreter runs (bash x.sh,pwsh -File x.ps1,python x.py,node x.js,cmd /c x.cmd; inline-c/-Command/-ehas no file). It locks those files against writes and deletes until the child exits, and sends their paths asbound_paths. The Agent hashes them at approval time and returnsbound_files;cw injectchecks the hashes again just before the start and does not run on a mismatch. The transient key includes the hashes, and a session grant covers only the files and hashes a person approved. A changed file opens a new popup with the headingScript changed after approval, and the decision row gets reasonScriptChanged. Each shim locks the pinned binary and checksreal_sha256just before the start; the lock lasts the run, so a pinnedaz.cmdcannot change whilecmdreads it. - Hidden PowerShell (#31): on
AuthorizeandReleaseSecret, the Agent reads the command line of eachpwsh.exe/powershell.exein the caller chain, up to and with the launcher, and parses it with the PowerShell parser (System.Management.Automation.Language). Opaque:-EncodedCommand(any accepted prefix),-Command -(stdin),Invoke-Expression/iex,&or.on a name built at run time,[ScriptBlock]::Create,InvokeScript/NewScriptBlock/ExpandString, a method name built at run time, a nested PowerShell with an opaque part, code that does not parse, and a-Filescript that cannot be read or holds any of these. An opaque part forces the Approval Gate whatever the policy level. The card heading isHidden PowerShell commandwith the reason, no transient or session answer covers the call, the card offers no session, and the decision row gets reasonHiddenCommand. A plaingh pr listkeeps its normal policy. - Windows Hello (#24): after a real Approve, the popup asks for Windows Hello (fingerprint, face, or PIN) through
UserConsentVerifierwhen the policy says so.policy.jsonfieldhello, set withcw policy hello off|secret-reveal|write-and-up(defaultsecret-reveal), shown bycw policy list. Verified → the approve stands, audit reasonHelloVerified. Cancel, retries exhausted, or device busy → Deny, audit reasonHelloCanceled. No Hello device, not set up, or disabled by policy → the plain popup decides, audit reasonHelloUnavailable. The helper adds a flag to its exit code:0x10verified,0x20unavailable (on an approve),0x40cancelled (on a deny). Any other combination fails closed. - CI: scripted modes via
CW_APPROVAL_MODE. - Transient reuse: a human
AllowOnce/Denyis reused, in memory only, for an exact retry (same launcher pid + start time, tool, class, secret name, command line) while the launcher process lives;CW_TRANSIENT_REUSE_SECONDSadds an optional time cap; audited with reasonTransientReuse. Policy auto-allow andUnavailableare recomputed every call. - Invalidation: all memory clears on agent stop and on workstation lock (
SessionSwitch/SessionLock).cw policy setclears entries whose launcher key or tool matches;cw policy unenrollclears that launcher key; acw harden <tool>that re-pins the binary clears that tool. Cleared through the existing policy-store and pin-store call sites - no polling. - Visibility / revocation:
cw policy sessionslists active session allows (id, launcher key + kind, pid, tool, secret name, class, granted / last-used / idle-expiry);--revoke <id>/--revoke-allwithdraw them and print the count. Backed byListSessionAllows/RevokeSessionAllowRPCs - names only, no prompt. Transient entries are never listed. The Secret Gates tab mirrors the list read-only under the Defaults card. - Release signing (#42): the release signs every exe and dll with the company name CmdWarden, in the zip and inside the nupkg, and the two install scripts. The Session Agent starts only an Approval Gate with its own signer, when it is signed. Every signature check uses WinVerifyTrust, so a copied signature block does not count. See Release signing.
- Tray icon (#43):
CmdWarden.SecretsManager.exe --trayruns one tray icon per user (own mutex). It pollsListSessionAllowsevery 5 seconds and lists each grant; Revoke and Revoke all callRevokeSessionAllow. It reads new audit rows and shows a notification forDenyCooldownandCanaryHit, one per launcher, tool, and reason each minute.cw shortcut installwrites the Startup entry and starts the icon;cw shortcut removeand the uninstaller delete the entry. - Policy edits in the Vault (#44): the Secret Gates page enrolls a launcher (Ctrl+E; the dialog lists unenrolled launchers from the audit), sets a level per tool (click the level pill), and unenrolls a launcher (Unenroll button, or select and Del). Each change has a dialog step and writes the policy file through PolicyStore, the same as
cw policy. The agent diff on its next load drops the matching approval memory. A kind change now counts as a launcher change too.
Issues: Windows Hello / step-up.
8a. Leak guard and canary tokens¶
Leak guard (#27). The Session Agent holds the real values, so it finds a leak with no guess.
CheckLeak { texts, source }returns each text with every CmdWarden vault value (CmdWarden/*, at least 8 characters) replaced by[CmdWarden: NAME], plus the matched names. Values never leave the Agent.- Each match writes an audit row: decision
redact, reasonLeakRedacted, toolleak-guard, the secret name, the launcher, and the source as purpose. cw leak-guard install claudeadds a Claude CodePostToolUsehook (matcher*) to~/.claude/settings.json. The hook sends every string oftool_responsein one call and returnsupdatedToolOutputwith the same shape, so the model sees only the placeholder.cw leak-guard install cursoraddsbeforeReadFile,postToolUse, andafterShellExecutionto~/.cursor/hooks.json. Cursor can block a read (permission: deny) and replace MCP output (updated_mcp_tool_output). It cannot change shell output, so a shell match only adds a note for the model.cw leak-guard install grokadds aPostToolUsehook (matcher.*) to~/.grok/hooks/cmdwarden.json. Grok reads the sameupdatedToolOutputreply as Claude Code. The policy hook is aPreToolUseentry in that file (matcherBash|PowerShell|Read). Grok maps those names torun_terminal_commandandread_file. A file read usestool_input.target_fileortool_input.file_path.cw mcp install grokadds[mcp_servers.cmdwarden]to~/.grok/config.toml. The command is thiscwand the argument ismcp. A table the user already wrote is left as it is.- When the Agent is down, the hook lets the output pass and shows a note. It never blocks the harness.
- Residual: only exact values; an encoded copy (base64, URL encoding) passes.
Canary tokens (#29). A fake token that no normal work uses is a clear sign of an attack.
cw canary install [--env <file>]...savesGH_TOKEN_BACKUPin the vault, appends a[backup-admin]profile to~/.aws/credentials, and appendsGITHUB_TOKEN=to each named .env template.canaries.jsonunder the product root records each value and the exact text added.- The Agent treats a canary as used when: a leak guard text holds a value, a shim argv holds a value, the SHA-256 of a shim caller env value matches (
env_value_hashes, never values), orReleaseSecretasks for the canary vault entry or names a value on its command line. - On a use: the Agent clears every grant and remembered answer of that launcher key, blocks the launcher process for every tool (default 1 hour,
CW_CANARY_COOLDOWN_SECONDS, or until the launcher restarts), writes adenyrow with reasonCanaryHit, and shows an alarm card in the lower right corner (interactive gate only). cw canary removetakes out the exact text it added (or each line with a canary value, when someone edited it), deletes the vault entry when it still holds the fake value, and emptiescanaries.json.cw canary statuslists the places.
9. Audit¶
| Item | Spec |
|---|---|
| Location | %LOCALAPPDATA%\CmdWarden\audit\ |
| Format | NDJSON (one JSON object per line) |
| Events | Gate decisions only (auto-allow, allow-once, deny, unavailable, session-grant, session-allow, redact) |
| Fields | ts, decision, reason_code, tool, command_class, policy_level, launcher_policy_key, launcher_kind, enrollment_kind, secret_name; optional purpose/path/pid |
| Never log | Secret values, full argv, env dumps |
| Retention | ~30 days, prune older |
| Access | cw audit |
| Fail closed | No secret release if audit cannot be written |
Issue: audit log storage and retention.
10. Install and distribution¶
| Item | Spec |
|---|---|
| Vehicle | dotnet tool (global, per-user) |
| Package | One package: CLI + Session Agent, same version |
| Scope | Per-user only - no admin / machine-wide v1 |
| Feed | Local/CI nupkg for handoff; nuget.org deferred |
| Update | dotnet tool update -g |
| Install vs harden | Install = binaries only; harden/setup adds Path + shims |
Issue: install and distribution.
11. CLI surface (v1)¶
cw / cmdwarden:
| Command | Role |
|---|---|
| help, version | Meta |
| doctor | Agent health / pipe |
| whoami / identity | Launcher identity |
| agent start | stop | status | Explicit agent control |
| save, inject, delete | Vault |
| policy list | enroll | set | unenroll | path | sessions | Enrollment, levels, and session-allow list/revoke |
| audit | Tail gate log |
| scan | First-catalog detectors |
| harden \<tool> | Opt-in shim + pin for catalog tools |
| launch \<harness> | Start Claude Code, Codex, Cursor, or Grok without ambient token variables (#25) |
Primary journey: install → doctor → enroll → harden gh → day-to-day shim use; plus scan / audit / manual vault.
Issue: v1 cw CLI command surface.
cw launch (#25)¶
- Catalog:
claude(Claude Code,claude.exe),codex(Codex,codex.exe),cursor(Cursor,Cursor.exe),grok(Grok,grok.exe). - The harness binary is the process that starts shells, so it is the launcher CmdWarden sees. A native install on PATH is that binary. For an npm
.cmdshim,cw launchstarts the shim and looks for the binary in the npm package the shim names. Cursor starts from%LOCALAPPDATA%\Programs\cursor\Cursor.exe. - The child environment is a copy without
AmbientEnv.All, the list the scan detectors use:GH_TOKEN,GITHUB_TOKEN,GH_ENTERPRISE_TOKEN,GITHUB_ENTERPRISE_TOKEN,GH_PATH,AZURE_CLIENT_SECRET,AZURE_CLIENT_CERTIFICATE_PATH,AZURE_FEDERATED_TOKEN_FILE,DOCKER_AUTH_CONFIG.cw launchprints the names it removed, never values. - When the binary's policy key has no enrollment,
cw launchenrolls it asai_harness. An existing enrollment stays as it is. - A running Cursor gets a new window in the old process, with the old environment;
cw launch cursorsays so. cw shortcut installadds<Harness> (CmdWarden).lnkto the Start Menu for each harness on this PC;cw shortcut removedeletes them.
12. Scan engine¶
- Coded C# detectors in product (versioned with tool) - not downloadable rule packs.
- Structured findings: id, tool, severity, title, summary, evidence (no secrets), remediation / harden_hint.
cw scanon demand; first catalog (gh/git/az/docker) + light system checks.- Read-only; hints at
cw harden …; no auto-harden; no FS watcher in v1. - MCP and harness config (#28):
mcp.plain_secretreads~/.claude.json(top level and per project),~/.claude/settings.json,~/.cursor/mcp.json, Claude Desktopclaude_desktop_config.json, VS Codemcp.json, and in the working folder.mcp.json,.claude/settings*.json,.cursor/mcp.json,.vscode/mcp.json. A finding is anenvorheadersvalue with a known token prefix (ghp_,github_pat_,sk-,xoxb-,AKIA, and others), aBearerortokenheader, or a secret-like key name (TOKEN,SECRET,API_KEY, ...). Placeholders (${VAR},<...>,[CmdWarden: ...]) do not count. Evidence names the file and the key, never the value. -
Move to vault: for an
envvalue of a server that starts a command,ScanFinding.Fixcarries the location.cw scan --move-to-vaultand the Move to vault button on the Detectors page save the value as vault secret<KEY>(a different existing value stops the move before any change), set the file value to[CmdWarden: <KEY>], and wrap the server ascw inject --tool mcp --class read +<KEY> -- <command> <args>. The server gets the value at start from the vault;cw policy set <key> mcp <level>controls it. Headers and remote servers get the finding without an automatic fix.cw scan --move-to-vaultstops when Claude Code runs, because Claude Code rewrites~/.claude.json. -
Vault age (#97):
vault.old_entrynames each vault secret whose value did not change for 90 days or more, with its age in days. It reads the last write time from Credential Manager, never the value. Canary entries, thecw trytoken, and TOTP keys of the proxy do not count.
Issue: scan engine shape.
13. PATH shims (common)¶
Research: path-shim-patterns.md.
- Per-user shim dir on user Path (prepend).
- Shim first on PATH:
HardenedToolStatus.Probecomposes machine then user registry entries and reports Degraded when an earlier entry holds<tool>.exe|cmd|bat.cw hardennever elevates.cw doctor --fix-pathrunscw doctor --fix-path --elevated <entry>once under UAC; that run prepends%LOCALAPPDATA%\CmdWarden\shimsto the machine PATH asREG_EXPAND_SZ(idempotent, vendor entries untouched) and broadcastsWM_SETTINGCHANGE. Doctor then reads the logon environment block (CreateEnvironmentBlockfor the current user); when the block does not expand the entry, a second elevated run writes the literal shims path instead. On success doctor printsshims first on PATH (machine)and re-probes. A non-default product root always uses the literal path. - Prefer multiplexed signed shim; tool id from image name.
- Harden time: absolute real path + SHA-256 (+ optional Authenticode).
- Runtime: full-path spawn only - never re-search PATH for the real tool.
- Residual (all tools): absolute path bypass of PATH harden; same-user tamper.
14. First catalog harden designs¶
All compat mode by default. Strong mode is opt-in per tool with --strong for gh, git, docker (plan first-catalog-strong-mode-plan.md), and az (#26). Strong mode moves the stock store into the vault and deletes the originals; cw unharden writes them back.
14.1 gh¶
Research: gh-windows-harden.md. Issue: #22.
| Item | Design |
|---|---|
| Store | CredMan gh:host |
| Harden | Pin real gh.exe; PATH shim; import active token into CmdWarden vault |
| Runtime | On allow: child always gets GH_TOKEN from vault |
| Classes | export (auth token, show-token, git-credential get) → secret-reveal; side effects / most auth → write; read-mostly → read; unmatched → unknown |
| Strong | cw harden gh --strong: stock gh:<host>:<user> entries and oauth_token lines move to CmdWarden/gh/* and are deleted; per-host GH_TOKEN / GH_ENTERPRISE_TOKEN injection; auth login / refresh / logout / switch get no token env and migrate after exit 0 |
| Residual | Absolute path; leftover gh: keyring (compat only); parent ambient env; gh auth switch fails in strong mode |
Spike implement shipped: issues #7 (shim pin), #8 (harden gh), #9 (classifier table), #10 (audit wiring).
14.2 git¶
Research: git-windows-harden.md. Issue: #23.
| Item | Design |
|---|---|
| Store | GCM / git:https://… CredMan (typical) |
| Harden | Pin real git.exe; PATH shim; leave GCM in place |
| Runtime | No GH_TOKEN-style inject; gate process only; allowed ops still use ambient GCM |
| Classes | credential fill → secret-reveal; push → write; fetch/clone/local read → read |
| Strong | cw harden git --strong: GCM entries move to CmdWarden/git/* and are deleted; git-credential-cmdwarden.exe is the only global credential.helper; the Agent checks the signer walk up to the pinned git.exe; store equal is a no-op, erase needs an equal value |
| Residual | Absolute path; IDE git; allowed AI push uses full GCM tokens (compat only); SSH; WSL |
Independent of gh harden - both required for first catalog.
14.3 az¶
Research: az-windows-harden.md. Issue: #24.
| Item | Design |
|---|---|
| Store | MSAL DPAPI under ~\.azure (not CredMan) |
| Harden | Pin real az.cmd (+ python); shim az.cmd/az.exe; leave MSAL cache |
| Runtime | No user-token env inject; gate only |
| Classes | account get-access-token → secret-reveal; login/CRUD → write; list/show → read |
| Strong | cw harden az --strong (#26): the login files of the az config dir (AZURE_CONFIG_DIR, else ~\.azure): azureProfile.json, msal_token_cache.*, service_principal_entries.*, msal_http_cache.bin, plus copies of config and clouds.config, move into %LOCALAPPDATA%\CmdWarden\az\login.bin, protected with DPAPI (current user) plus a CmdWarden entropy value; the login files are deleted from the stock dir, so a plain az has no login. An allowed shim run gets a run folder az\runs\<shim pid>-<start ticks> with the files, named in the child env as AZURE_CONFIG_DIR (AZURE_EXTENSION_DIR stays at the stock cliextensions); the audit row names az/login. After the run, whatever the exit code, the shim calls MigrateToolStore { tool=az, run_dir }; only the shim process that got the folder can hand it back, the store takes the files (a logout removes the login), and the folder goes. Each strong az run sweeps folders of dead shims. Doctor reports Degraded when a login file is back in the stock dir (stock az login returned). cw unharden az writes the files back (settings changed since stay), deletes the store and run folders, and removes the pin and shim. Residual: two overlapping runs; the last capture wins |
| Residual | Absolute path; same-user DPAPI; SP env; Azure PowerShell out of scope |
14.4 docker¶
Research: docker-windows-harden.md. Issue: #25.
| Item | Design |
|---|---|
| Store | Desktop credsStore / wincred; optional base64 auths |
| Harden | Pin real docker.exe; PATH shim; leave Desktop store |
| Runtime | Optional child DOCKER_AUTH_CONFIG from vault on allow; never leave in parent |
| Classes | login/push → write; pull → read |
| Helpers | docker-credential-cmdwarden.exe gates registry credentials when credsStore is cmdwarden; the pinned docker.exe must sit at depth 1, or at depth 2 behind compose or buildx |
| Strong | cw harden docker --strong: Docker Credentials entries and inline auths move to CmdWarden/docker/* and are deleted; credsStore: cmdwarden; no DOCKER_AUTH_CONFIG overlay; foreign credHelpers stay with a warning |
| Residual | Absolute path; foreign credHelpers; ambient env; WSL |
15. Threat notes (summary)¶
The Threat model page has the full list, with the status and the test of each attack.
| Threat | Mitigation |
|---|---|
AI harness dumps tokens (gh auth token, git credential fill, az account get-access-token, helper get) |
Command class secret-reveal + policy (AI default Read) + Approval Gate / block |
| Ambient long-lived env tokens | Child-only inject where applicable; scan profiles; cw launch <harness> starts the harness without them (#25) |
| Same-user CredRead / DPAPI | Agent policy is the gate; strong mode empties the stock store for gh / git / docker / az; residual accepted in compat |
| Absolute path bypass of PATH shim | Scan + guidance; WDAC out of PATH-only v1 |
| Secret values in tool output read by the model | Leak guard hooks replace each vaulted value with [CmdWarden: NAME] (#27) |
| Prompt injection that hunts for tokens | Canary tokens raise an alarm and block the launcher (#29) |
| Script or binary changed after approval (TOCTOU) | Approval binds file hashes; files stay locked until the child exits (#30) |
| Wrapper bypass | PowerShell wrappers in the caller chain are parsed; hidden code forces the Approval Gate (#31). Other wrappers stay a residual (PATH-only) |
| Approval clickjacking, self-approval by injected input | Native UI; Approve accepts only non-injected hook input, no UI Automation Invoke (#23); phone OOB out of scope |
Broader Automic comparison: automic-vault-architecture.md.
16. Explicit non-goals (v1 / this handoff)¶
- 100+ tool harden catalog
- Phone / out-of-band approval
- API hooking / detours
- CLI flag parity with macOS Automic Vault
- Session 0 Windows Service as agent host
- Upstream multi-OS monorepo with automic-vault
- Enterprise MDM / multi-user org as first-class driver
- Polished tray Settings app
- Windows bless / capability-bundled scripts (future note only)
- Timed session grants ("allow write for 10 minutes") and a live management strip in the shell; persisting approval memory across agent restarts (approval memory v1)
- Downloadable scan rule packs; continuous scan watcher
- Machine-wide install, winget/MSIX as primary, auto-update, nuget.org requirement
- Strong-mode store strip as default for git/az/docker
17. Implementation status and next build work¶
Done¶
- Spike foundation: bootstrap, Session Agent health, vault save/inject, hybrid identity, policy gate, Approval Gate (#1 to #6).
- Spike vertical: gh shim,
cw harden gh, gh classifier, audit trail (#7 to #10). - First-catalog compat harden for
git/az/docker, scan detectors, dotnet tool packaging, management shell tabs. - Approval memory and the shipped steps of the Windows gaps plan.
- First-catalog strong mode, every slice (first-catalog-strong-mode-plan.md section 4).
Next build work¶
The open steps of automic-windows-gaps-plan.md section 4.
Decision index (wayfinder)¶
See map CmdWarden product handoff (beyond spike) Decisions so far for one-line gists and links to resolution comments (#13–#25).
18. Related documents¶
| Doc | Role |
|---|---|
| the Glossary | Ubiquitous language |
| Home | Build and run |
| docs/user-guide.md | End-user guide |
Git history before 14e5e6b |
Research notes and prototypes. The UI implement plans are in the history of docs/spec/ |
| GitHub issues | Spike implement + closed decision tickets |
End of handoff specification.