CredsForDevs
Your SSH hosts and keys, VPN configs, database connections, passwords and the CLI commands
nobody remembers a week later — in the editor you already have open.
Zero-trust is a property of the design here, not a promise. Secrets live in the OS
keychain. Anything that leaves your machine is encrypted in the editor, under a PIN or a
security key that never leaves it — so the folder, the NAS or the server holding your vault
holds ciphertext and nothing else. Whoever runs that storage, including you, cannot read
what is in it.
Creating a profile signs in once with the Microsoft or Google account you already have — that
identity is only the profile's name, nothing is stored with them, and there is nothing to sign up
for. After that it works with no server and no network: nothing leaves the machine. Point it at
a folder to sync your own machines; add the optional self-hosted server when a team needs to share.
Where it runs, and two honest caveats.
The extension is pinned to your local machine ("extensionKind": ["ui"]). Under Remote-SSH,
WSL or a Dev Container it therefore keeps running on your own computer, which is the only place
~/.ssh, your keychain and your VPN client actually are. The cost is that Share with Claude
Code reaches an agent running on that same machine — an agent running inside a container
cannot see the broker's loopback port. Run Claude Code on the client for that feature.
Secrets go to vscode.SecretStorage, which is the OS keychain when there is one. On a Linux
box with no reachable Secret Service — headless, a minimal container, a WSL distribution, SSH with
no D-Bus — VS Code silently falls back to a basic store that is obfuscated rather than encrypted,
and says nothing about it
(microsoft/vscode#204552). A session bus is
not enough on its own: the store is chosen from the desktop environment before anything is
tried, so a session advertising none gets the basic store however much is installed. Install
gnome-keyring or kwallet on those machines, make sure it runs in the session VS Code is
started from, or treat that vault as unprotected at rest.
Everything it does
|
|
| SSH in one click |
The stored password is supplied to ssh through SSH_ASKPASS — never retyped, never on a command line, never in scrollback. A key kept in the vault is written out 0600 for that session and deleted when the terminal closes |
| SSH keys |
Store the key pair itself, or point at a file on this machine. One connection can borrow another entity's key. Install SSH Key to System writes the pair into ~/.ssh — dir 0700, private 0600, public 0644 |
| SSH agent, confirming every use |
Add to SSH Agent serves a stored key from memory over a socket of this window's own, so no key file exists on disk at all — and every single use opens a dialog naming the key and what it is signing: an SSH login as user, or a Git commit. New terminals get SSH_AUTH_SOCK, so ssh and git just find it |
| Git commit signing |
Copy Git Signing Config gives you the gpg.format ssh lines with the public key inline, so Git signs commits with a key that lives only in the vault |
| One-time codes (TOTP) |
Paste the otpauth:// URI or the base32 secret from any service's "can't scan the QR code?" screen. The viewer shows the live code with a countdown; the tree has Copy One-Time Code. Steam Guard too — so the second app can stay closed |
| Secret references + masked run |
Write creds://you@corp.com/prod-db/password where a value would go in a command or a script, then Run with Secrets: the value reaches only the child process's environment, and every appearance of it in what that process prints is replaced with a <CREDS_MASKED:NAME> marker naming which one it was |
| Generate, don't invent |
Passwords, passphrases and Ed25519 key pairs, made in the form with the OS's own randomness and reported in bits rather than as a coloured bar. A generated key is saved straight to the keychain — ssh-keygen cannot do that, it writes a file by definition |
| Import what you already have |
~/.ssh/config, and CSV or JSON exports from Bitwarden, 1Password, KeePass, LastPass and Termius. Every skipped row is reported rather than quietly dropped |
| Health report |
Reused passwords, weak ones, unencrypted keys in ~/.ssh, plaintext credentials in a workspace .env — all on this machine. An optional breach check sends five characters of a hash and nothing else |
| Keyboard and orientation |
Ctrl+Alt+P jumps to any credential by name, plus filter, copy, connect and lock bindings, a walkthrough, and a status-bar item that says whether the vault is open or locked |
| VPN |
OpenVPN / WireGuard / IKEv2 / L2TP / other, with host, login, port and a key or certificate. The config file is a secret like any other; Start VPN / Stop VPN bring the tunnel up and down through the OS's own elevation prompt |
| Databases |
postgres / mysql / mssql / mongodb, entered as a connection string or as fields that rebuild it in the right dialect. Open in DB Extension hands the connection to Database Client, MongoDB for VS Code or the SQL Server extension |
| Terminal commands |
aws sso login --sso-session OD-org is unfindable in shell history a week later, and the part you forgot is never the verb. So each argument is a row with its own note and a tick to keep a flag without using it. Run in Terminal, Copy Command, Show Command and Notes |
| Plain credentials |
Anything that is just a login and a password — plus a login and a URL stored encrypted like the password and shown in clear on the card — with notes that live in the keychain rather than in plaintext metadata |
| Attachments |
Every entity can carry one encrypted file (PDF, Office, text, archives — the executable family is refused, double extensions included) and one encrypted image, 4 MB each, shown as a zoomable preview |
| Environment variables |
Export a secret field into every new integrated terminal under a name you choose. The name syncs; the value is written only from the local keychain, so a binding arriving by sync is a name waiting for a value, never a secret in transit. A ✓? button echoes it in a fresh terminal so you see it |
| A terminal client |
creds ssh prod-db -- uname -a from iTerm, Alacritty, tmux — a native binary that holds no secret and can obtain none. It relays the request to the VS Code window that owns the entry, which performs it and returns only the output. Inside WSL it hands the call to the Windows binary through interop, so nothing new listens anywhere and no networking has to be configured |
| Share with an AI agent |
Share with Claude Code… lets a coding agent run commands on an SSH host without ever receiving the password or key — see below |
| Team sharing |
Send one entity, or a whole folder, sealed to a colleague with a one-time PIN. Create Entity for… authors one directly for someone else |
| Multi-machine sync |
One AES-256-GCM file per profile, merged causally (version vectors) rather than overwritten, so two machines editing at once converge on the same answer |
| Short-lived entries |
An entry can carry a lifetime: a clock, one agent use, or this window's close. When it ends, the entry is really deleted — tombstone, every stored secret, and its version history — so the deletion travels to your other machines like any other. A "burned" flag that left the secret in place would still be readable from history and would come back on the next sync |
| Git sync |
Point an account at a private git repo (git@github.com:me/vault.git) and the encrypted vault is committed and pushed on change, pulled on every cycle — the same causal merge as every other transport. Needs git on PATH. Commit messages carry only an account hash and a time, never anything about what is inside; a reader of the repo can infer when a vault changed, never what |
| Dated snapshots |
Separate from sync and deliberately so: sync merges, and a merge propagates a deletion. A snapshot is the copy that still has the thing you deleted |
| Security keys |
Open the vault by touching a YubiKey (WebAuthn PRF) — several keys plus the PIN, any of them opens it, adding or removing one never re-encrypts your data |
| Corporate recovery |
For a team on a self-hosted server: the operator names recovery officers, and any two of three (configurable) can together open a vault whose owner has left — without the server ever being able to. The organisation's key is split between them at setup and destroyed; each share is sealed under an officer's own PIN or YubiKey. Enrolment is automatic, so the extension tells you it happened, shows you who the officers are, and refuses to seal your key to a fingerprint this machine has not pinned |
| Recovery code |
The third way in, for the day the PIN is forgotten and the YubiKey is gone: a 150-bit code shown once, printed, and kept on paper. It opens the vault on its own and then offers you a new PIN. Shown with a Print button and no way to copy — deliberately, because a clipboard is read by managers, sync tools and screenshot pipelines, and this is the factor that has to outlive the laptop. Generating a new one retires the old printout |
| Auto-lock |
Locks after an idle window measured in your actions, not mouse movement and not background sync |
| Masked agent output |
A secret this extension supplied to run a command cannot come back out in that command's output: the broker replaces it with <CREDS_MASKED:NAME> on the way out. Exact values only — never a guess about what looks like a token, because a wrong guess corrupts the diff or the JSON the agent then acts on. It covers commands run through CredsForDevs, and says so rather than implying it guards every channel. Three limits stated plainly, because a security feature that overstates its reach is worse than one you understand: it does not watch the clipboard continuously (VS Code exposes no clipboard-change event — there are two on-demand commands instead, Check Clipboard for Secrets and Scan File for Vault Secrets), it does not see a file the agent opened by itself, and it cannot help against Windows Clipboard History, which captures at copy time before anything of ours runs |
| Filter the tree |
The first row of the sidebar is a search box: type and the tree narrows live, folders holding a hit open themselves, accounts with nothing matching drop out, and the row says how many entries survived. It matches what a row already shows you — name, user, host, port, command — and never a secret: a filter over passwords would confirm one a keystroke at a time to anyone at an unlocked window |
| Config files as entries |
appsettings.Development.json or a .env lives in the vault instead of being passed between developers by hand: the body is a secret, validation saves anyway and marks the row until it parses, a Fields tab edits one value without touching your formatting, and Write Config File Here… materialises it to disk — refusing a path git tracks. Enable Code Access… mints a long-lived key (shown once; the vault keeps only its hash) and the viewer answers "how do I read this from code?" in twenty languages, naming the exact file the snippet goes into |
| Agents over MCP |
creds-mcp — a separate binary an MCP client starts — shows an agent exactly what you opened to it: two ladders, ten switches — six over entries, four over folders — all off by default and inherited by everything below the folder you set them on. An agent can list, use, rotate (through a {{creds:new}} placeholder, so no value ever enters its context), create into open folders and shape what gets generated (length, character sets, word count), delete to the Trash only, make and rename folders — and ask how code reads a config (creds_config_snippet, the same twenty-language catalog the viewer renders). An action raises the consent modal as often as the entry says — every time, once every 12 hours, or never, with creating and deleting always asking; MCP logs is the journal either way |
| Capability filters |
The tree filter understands what an entry CAN DO, not just what is written on it: has:totp, has:cli, has:env, has:code-access, has:deps, has:attachment, has:image, is:ephemeral, and mcp:visible / usable / rotate / create / delete-own / delete-any — combinable with free text (aws has:totp mcp:usable). Predicates read metadata only; an unrecognised one is named on the search row rather than silently matched as text |
| A missing tool becomes an offer |
Connect over SSH on a machine with no ssh client, or start a VPN with no WireGuard, and instead of a dead terminal you get a modal naming what is missing and — on Yes — a visible terminal running the platform's own install recipe |
| Remote Bridge |
On a Remote-SSH host the local window is unreachable by definition; Open Remote Bridge… holds an ssh -R tunnel so creds on the host reaches the window that holds the vault. Install creds on the Host… puts the binary there from the entity's own context menu |
| Inside WSL |
creds hands calls to the Windows binary through interop — nothing new listens anywhere. Set Up the WSL Agent Relay serves the SSH agent on a unix socket inside the distribution, so ssh and git in WSL can use vault keys with a confirmation dialog per signature |
| Text zoom |
± buttons on every CredsForDevs page scale the text up to five steps either way; the offset is shown (+3), stored in a synced setting, and every open page follows it at once |
| Diagnostics you can attach |
Show Diagnostics opens the one output channel and names the per-run log file on disk — sync, backup, transport and unlock failures land there, and no secret can, by construction |
| Several accounts |
Microsoft and Google profiles side by side, each with its own tree, its own vault location, and its own team — separated by a blank row, each account row counting entries / in the Trash / shared |
The repository page carries the
security model, the broker's design and every security review this code has been through.
The tree
Activity Bar → key icon. Top-level items are account profiles added via
the VS Code Authentication API (Microsoft is built-in; Google via the
extension's own OAuth provider — see below). Inside a profile: folders and
entities.
- Single click on an entity selects it and shows it in the read-only viewer — in one
shared preview tab that the next single click reuses, the way the editor previews files:
ten clicks on ten entries leave one tab. The viewer shows only the fields that actually hold
a value, each with a copy-icon button (secrets stay masked; copying goes through
SecretStorage, never through the page), a download icon on the VPN config row (Save As),
plus a kind-aware Copy All.
- Double click pins that preview into a tab of its own (and opens one when nothing is
previewed), so two entries can be compared side by side. The twisty of a row with history or
dependants is the workbench's: a double click still toggles it, as it does in every tree.
- Green ▶ (hover, SSH entities) connects SSH; green database icon
(DB entities) opens the entity in a DB extension. Nothing ever runs on a
plain click.
- Right-click menus are capability-filtered: an entry only offers what
it can actually do (
Connect SSH/Toggle SSH need a host, Copy Password needs a stored password, key/VPN/DB actions need those kinds).
- Move items between folders by drag & drop or Move to Folder…
(within one profile). The toolbar + buttons create at the profile
root; the inline + on an account/folder row creates inside that row.
Folders: types and ordering
- Default folders for a new account: the first time you add an account, and
only when it starts out empty, it is seeded with five typed folders —
db
(Database), vpn (VPN), ssh keys (SSH key), ssh connections (SSH
connection) and passwords (Credential). This is one-time: if the account
already has data (e.g. a returning user whose vault is pulled from the NAS
first), or once you rename or delete the defaults, they are never re-created.
- Creating a folder asks for its content type — Credential (default),
SSH connection, SSH key, VPN, Database, or Any type; right-click →
Change Folder Type… updates it later. The folder row shows the
type's icon (lock/remote/key/shield/database) and name.
- A typed folder holds only its own kind: entities created inside it
get the form's Type selector preset and locked, and moving/dragging a
mismatched entity in is blocked with a warning. Existing entities are
never retro-converted.
- Manual ordering: right-click a folder → Move Up / Move Down.
The order persists and syncs across machines; untouched folders stay
alphabetical after the manually placed ones. Entities stay alphabetical.
Entities
Add Entity / Edit opens a single webview form with a Type selector
— Credential (default) / SSH connection / SSH key / VPN / Database /
Terminal command / Script — and
only the chosen kind's section is shown. Saving scrubs the other kinds'
fields, so switching type leaves no stale data. Inside a typed folder the
selector is preset and locked. Validation errors render inline, and any
script failure is printed into the form's error area (never a silently
dead form).
- Password / secret value →
context.secrets (OS keychain), key
${accountId}_${entityId}. Never in globalState, settings, or logs.
- SSH private key content → SecretStorage
(
${accountId}_${entityId}:sshPrivateKey); public key → metadata.
- SSH key path — alternative to content: a pointer to a key file on
this machine's disk (the path syncs, the file does not).
- SSH key source — an SSH connection can reference another entity as
its key. Resolution order when connecting: referenced entity → stored
key content (materialized to a
0600 file under extension storage) →
plain key path.
- Install SSH Key to System (entities flagged as SSH keys): writes the
pair into
~/.ssh — dir 0700, private 0600, public .pub 0644 —
with an overwrite confirmation.
- One encrypted file and one encrypted image, on an entity of any kind.
Additional file takes what people actually attach — PDF, Office, text, data,
archives — and refuses the executable family outright, including as the tail of a
double extension (
invoice.pdf.exe). Both are capped at 4 MB, checked before
anything is stored, and both live where every other secret lives: the OS keychain
locally, the sealed vault in transit — sync, backups and snapshots carry them like
passwords. In the viewer a stored file is a row with a save button and a stored
image is a 200×200 preview (click to zoom ×2, twice; a third click resets).
Only the file name is plaintext metadata; the content never is.
In edit mode stored secrets are never shown or sent into the webview:
leaving a secret field empty keeps the current value; explicit "clear"
checkboxes remove it.
VPN entities
Pick the VPN type in the form: choose the protocol (openvpn /
wireguard / ikev2 / l2tp / other) and upload the config file (.ovpn / .conf — the
picker runs in the webview, so it browses the client OS's files, i.e.
Windows even under WSL). The config content is a secret: SecretStorage
locally, inside the AES-256-GCM payload on the NAS — never plaintext in a
backup; only the original filename is metadata. VPN entities show a shield
icon; Save VPN Config As… writes the decrypted file back out for your
VPN client; the viewer shows the type and a masked, copyable config.
A VPN entity also carries host / gateway, login, port, and a key or certificate — the key
goes to the OS keychain like an SSH private key; host, login and port travel only inside the
encrypted vault. The viewer shows exactly the fields that are filled; an empty one adds no row.
Start VPN / Stop VPN bring the tunnel up and down. The config is materialized to a
0600 file, the launcher is located on this machine (wg-quick, wireguard.exe,
openvpn, or the OpenVPN Connect GUI on Windows — recognised as the different product it is,
rather than reported as a missing binary), and the composed line is shown in a terminal
rather than run silently. The extension never elevates itself: Windows raises its own UAC
prompt, POSIX its own sudo password prompt. That dialog is the trust boundary, and it
should be. The terminal it opens runs this machine's own shell (PowerShell on Windows, bash
elsewhere) whatever your default terminal is — a WSL-bash default no longer receives a
PowerShell line.
Started by lets one of your own Terminal entries start the VPN instead of the built-in
launcher: its command runs with {config} replaced by the path of the stored config. That makes
any VPN startable from the tree — IKEv2 through rasdial, a vendor's own client, OpenVPN from a
path nobody probes. Stop explains where to stop it; the extension does not guess a process.
Database entities
Pick the Database type in the form: choose postgres / mysql / mssql /
mongodb and fill either the connection string or the component
fields (Host / Port / Database / User / Password) — they are two-way
linked: typing the string fills the fields, editing a field rebuilds the
string in the right dialect (URI for postgres/mysql/mongodb,
Server=…;Database=… for mssql). The port is optional with per-type
default placeholders (5432/3306/1433/27017); a host pasted with a scheme
(http://…) is auto-cleaned. The string is the single stored secret —
prefilled and editable in Edit mode (the one deliberate secret-in-webview
exception); emptying it clears it. The viewer shows the parsed parts with
copy buttons (password masked, default port labeled), and Copy All
emits type, string, and all parts. The inline green database button =
Connect opens the entity in a dedicated DB extension:
- Target resolution: your override in
credSshManager.dbExtensions
(e.g. { "mysql": "cweijan.vscode-mysql-client2" }) → the first
installed candidate → the recommended default, offered with a one-click
Install when missing.
- Launch: the connection string is copied to the clipboard, the target is
activated, and its add-connection flow opens — Database Client
(
mysql.connection.add; flip its "Use Connection String" toggle and
paste, all fields fill at once), MongoDB for VS Code
(mdb.connectWithURI, takes the URI directly), SQL Server
(mssql.addObjectExplorer); for other extensions the command is
discovered from their own manifest. The post-open notification carries
the exact paste instruction per target.
- Honest limitation (verified against Database Client's source: no
exported API, no settings store, no URI handler): extensions without a
public connection API get the open-form-plus-one-paste treatment — we
never write into another extension's private storage.
Terminal command entities
Pick the Terminal command type in the form. The case it exists for:
aws sso login --sso-session OD-org is unfindable in shell history a week later, and the
part you have forgotten is never the verb — it is which value belongs to which environment,
and why.
So an argument is a row, not a word inside a string: each has its own value, its own
explanation underneath it, and a tick that keeps a flag without using it (--debug is what
you want back next week; deleting it means retyping it from memory). Rows can be added,
removed and reordered, and a live preview shows exactly what will run.
- Run in Terminal (the same green ▶ as Connect SSH) runs the assembled line.
- Copy Command for the times you want to edit before running; Show Command and Notes
prints the line with every argument's explanation.
- The notes can fill themselves in. With
credSshManager.readCliHelp on (default), pasting
a whole command reads what each flag means by running <tool> --help. It runs only the tool
you just typed, with no shell and none of our own arguments, and only when every word of the
command is a plain tool name — anything carrying a shell metacharacter is refused rather than
probed.
- Runs on (Windows / macOS / Linux) says which system the command is written for. Your default
terminal runs it when it is a shell of that system — pwsh 7, zsh with your aliases — and that
system's own shell does otherwise; on another system it is refused. Not set keeps your default
terminal, exactly as before.
Execute what is executable first (in Depends on) turns an entry's dependencies into a chain:
before Run in Terminal, Run Script, Run with Secrets or Start VPN, its Terminal
dependencies run in order, each one awaited — "install openvpn", then "start openvpn", then the
VPN. You read the chain before its first run; a step that fails asks whether to go on; a circular
chain is refused. Waiting for a step needs VS Code 1.93 or newer.
Environment variables in the terminal
Each secret field — password, private key, public key, connection string, DB password — has a
toggle in Edit (off by default). Switching it on mints a name from the entity (entity git key,
private key → ENV_GITKEY_PRIVATEKEY); edit it if you want another. Saving writes the value
into every new integrated terminal, persistently across reloads.
What syncs is the name — a name is not a secret. The value is written only on the machine
that saved or pressed the button, from that machine's own keychain, so a binding arriving by
sync is a name waiting for a value, never a secret that travelled. Renaming or disabling a
binding deletes the old variable on save rather than leaving it set forever.
The viewer shows the variable's name with a copy button, a Set button that re-writes the
value on demand (the collection can be lost with extension storage, and recovering must not
require re-saving the entity), and a ✓? button that opens a fresh terminal and echoes
the variable — so it is seen rather than trusted from a notification. The probe's spelling
follows the actual default shell, not the OS: PowerShell gets $env:NAME, cmd %NAME%, bash
$NAME. Fair warning it carries: echoing prints the secret into that terminal's scrollback.
The SSH agent — a key that is never a file
Add to SSH Agent on a stored SSH key serves it from this window's memory over a socket of its
own (a named pipe on Windows). SSH_AUTH_SOCK is set in every integrated terminal opened
afterwards, so ssh and git find it with no configuration — and the 0600 file the extension
used to write for ssh -i stops existing. Connect SSH on an agent-served key passes no -i.
Every single use asks. The dialog names the key, its fingerprint, what is being signed —
an SSH login as a particular user, or a Git commit signature — and when it was asked
(Requested 2026-09-24 12:30:00 (UTC+03:00).), because "a key is being used" is not something
anyone can decide about. Unanswered for five minutes it refuses — the same bound as the agent
consent dialog — and a click after that signs nothing. Three answers: Allow once, Allow for 10 minutes
(a git push signs and authenticates in one breath, and two modals per push is how people learn
to click without reading), or Deny. Every request is a line in CredsForDevs: SSH Agent.
Git commit signing. Copy Git Signing Config puts the gpg.format ssh lines on the clipboard
with the public key inline (user.signingkey "key::ssh-ed25519 AAAA…"), so Git signs with a key
that exists in no file. Add the same public key to GitHub/GitLab as a signing key — forges keep
those separate from authentication keys.
Honest boundaries:
- A passphrase-protected key is refused, with the
ssh-keygen -p -N "" command that fixes it.
OpenSSH encrypts with bcrypt_pbkdf and Node has no implementation of it; the vault already
encrypts the key at rest, so storing it unencrypted here loses nothing.
- On Windows use the built-in OpenSSH (
C:\Windows\System32\OpenSSH). The ssh that ships
with Git for Windows is an MSYS build that cannot connect to a named pipe — the signing config
sets gpg.ssh.program accordingly, and this is measured, not assumed.
- Keys are held in memory only. Closing the window revokes everything, which is the whole
revocation story — the same as it is for agent grants.
- The agent is read-only: it lists keys and signs. A client cannot add, remove or lock anything.
One-time codes (TOTP)
Paste an otpauth:// URI — or the bare base32 secret — from the service's "can't scan the QR
code?" screen into the One-time code field. The viewer then shows the current code with a
countdown next to it, the tree offers Copy One-Time Code, and the copy expires from the clipboard
like every other secret here. Steam Guard's five-character variant is a checkbox.
The seed is a secret like a password: it lives in the OS keychain, rides the same encrypted
envelope in sync, backups and shares, and — the part worth stating — the webview never receives
it. What the panel is sent is the six digits, which stop being true in under a minute anyway.
Secret references and Run with Secrets
A script's variables never enter its text; they travel in the environment. What that could never
stop is the script printing them itself. This closes that, and extends it to commands:
creds://you@corp.com/prod-db/password
Write that reference as a script variable's value or a command argument, and Run with Secrets:
- resolves it from your vault at the moment of running, into the child process's environment
and nowhere else — the argument on the command line becomes
"$CREDS_REF_1", so the value is not
in the process list where every user on the machine can read it;
- runs it in a terminal this extension owns, and replaces every appearance of the value in what
the process prints with
<CREDS_MASKED:NAME>, naming which secret stood there — including a value split across two writes.
Reference a folder path (creds://you@corp.com/Servers/EU/gateway/privateKey) when two entities
share a name: a reference that could mean two things is refused, naming both, rather than
quietly picking one. Fields: password, privateKey, publicKey, dbConnection, dbPassword,
notes, totp.
What it cannot do, so you meet it here rather than in a bug: the terminal it runs in has no
PTY. A program that prompts interactively, draws a progress bar, or colours its output by detecting
a terminal will behave as it does when piped — use Run in Terminal for those. And masking is
textual: a program that base64s a value before printing it defeats it, which is why a script that
prints its own variables is still flagged when you save it.
Generating a secret instead of inventing one
The Secret section of the form has Generate password and Generate passphrase; the SSH
key section has Generate Ed25519 key pair. CredsForDevs: Generate Password or Passphrase…
does the same for a password that is not stored here at all, and puts it on the expiring clipboard.
- A password is 20 characters over all four sets by default, with one character guaranteed from
each selected set and the whole thing shuffled — so it satisfies a site's composition rule
without the rule being visible in the result.
- A passphrase is six words from a 256-word list: exactly eight bits each, 48 bits for the
default. The optional capital and trailing digit exist for sites that demand them and are not
counted as strength, because a number that flatters is a number you cannot use.
- Ask for a word length and the source becomes a longer list, filtered — and the strength is
recomputed from the pool that is actually left. A filter that shrinks the source and goes on
reporting the old number would be a lie about a password.
- A seed phrase is drawn with the wordlist and the word count you choose, with the checksum
computed for you, so what comes out is a phrase a wallet will accept. Word length is deliberately
NOT offered there: on a BIP-39 list the lengths belong to the list, and filtering them produces
something no wallet takes.
- The strength is stated in bits, not as a colour. A bar tells you how a designer felt; the bits
tell you how long a guess takes.
- A generated key pair is drawn in the editor and saved to the keychain.
ssh-keygen cannot do
that — it writes a file — and with Add to SSH Agent the key is then used without becoming one.
A PIN on one entry — a second lock inside the vault
The vault is one lock: everything inside it opens together, which is what makes it usable, and it
means an open vault is a readable vault. For the few things that deserve a second lock — the
production database, the payment card, the key that signs releases — right-click the entry and
choose Protect with a PIN.
Every secret that entry holds is re-encrypted under a PIN of your own: the password, the private
key, the notes, the login and URL, the payment details, the config body, the connection string, the
VPN configuration, the one-time-code seed. A fresh random key seals the value, the PIN seals that
key, and the plaintext is nowhere. This is a real re-encryption, not a prompt in front of a value
that is already readable.
- Opening the entry asks once. The PIN opens the whole entry for as long as that window lives;
closing it, reloading it or locking the vault forgets it. It is written down nowhere.
- Agents do not see it at all — absent from the listing and the search, and a direct request by
id is answered the way a made-up id is answered. An agent that can see an entry it can never open
will keep asking.
- Nothing automatic uses it. An environment variable, a terminal, the SSH broker, a
creds://
reference and an agent rotation each say why instead of guessing.
- The health report skips it and says so, because ciphertext always grades as a strong unique
password and grading it would tell you your weakest habit is fine.
- A backup keeps the protection — what is exported is what is stored, still wrapped.
Protect Every Entry with a PIN on a folder does the same to every entry inside it, at any
depth. It does not encrypt the folder — a folder is a place, and the protection belongs to the
entries. Entries that already have a PIN of their own are named before the run starts and then
skipped: a folder may legitimately hold entries under two PINs, and when it already holds
protected ones the PIN you type is checked against them and you are told how many it opens before
anything is written.
Sharing one asks you for its PIN: the values have to be unwrapped before they can travel,
because the person receiving them does not have your PIN and never will. Their copy is therefore not
protected by yours — they are asked to choose one of their own when they accept it, and declining
imports nothing. Your PIN never leaves this machine.
Running the folder command also sets that folder to ask for a PIN on every entry created in it
afterwards, including when the folder was empty at the time — the one case nothing else could tell.
Stop Asking for a PIN Here turns that back off; it changes nothing about the entries, which keep
their own PINs.
Remove PIN Protection takes it back off, given the PIN. And the one thing to read twice: a
forgotten PIN is gone data. There is no recovery — the vault recovery code opens the VAULT, not an
entry.
A password stored woven with a decoy
The General section of a credential has Store this password woven with a decoy. Tick it, pick
one of the twelve methods — the same twelve a card's PIN uses, offered in an order drawn afresh
every time — and what goes into the keychain is your password and a made-up one of the same shape,
interleaved.
The method is written down nowhere. Opening the entry shows two rows of characters with nothing
marking either one; you pick the method you chose and read the row you recognise. A wrong method
answers in exactly the same shape as a right one, which is the point rather than an omission: the
choice in your memory is the only thing separating the two halves.
- The decoy has the same length and the same character sets as the real password. A character
from a set your password does not use would be provably decoy.
- The entry says Woven — on on the edit form and in the viewer. What cannot be undone is the
unweaving — that needs the method. Replacing the password always works, and the box arrives
already ticked, so a replacement stays protected unless you untick it on purpose.
- Nothing automatic will use it. An environment variable, a terminal, the SSH broker, a
creds:// reference and an agent rotation are each given a sentence saying why instead of a
guess — a wrong password injected into a login is an account lockout nobody watches happen.
- A share carries the mark, so whoever you share with is told the value is woven rather than
opening an entry full of gibberish with nothing explaining it.
- Too short to weave, or no method picked, and the form asks before it saves rather than
quietly storing the password in the clear.
Payment instruments — and reading one back
A card, a set of bank details, or a phrase you must not lose.
- The card number is shown in the groups it is printed in — fours, or 4-6-5 for American
Express — and stored as digits, because a woven number is permuted character by character and a
stored space could never be unwoven. Two copy buttons: digits for a form that refuses spaces,
groups of four for reading it aloud.
- The payment system is detected from the number and is yours to correct. It has to be: a
number stored woven with a decoy has no first digits left to read it from.
- The billing address is six cells — paste a whole one and it is split into them, where every
guess is visible and correctable. The block underneath is assembled in the order the destination
country writes it, and that block is what a share and an export carry.
- A CVV or PIN asks before it appears, and can be put away again — the button becomes Hide.
Copying it asks nothing: Copy and Show are separate buttons, and only Show asks.
- Any of the number, CVV, PIN, IBAN or account number can be stored woven with a decoy under
one of twelve methods, and the controls now SHOW what a method does: a green column, an orange
one, and the weave of the two. Both are made up for the picture; your own value is never drawn
beside the decoy it is woven with.
- A method is called the same thing everywhere. The order the twelve are offered in is drawn
afresh each time, so a position never becomes a habit — but "Method 5" names one algorithm, in
the form where you weave a value and in the card where you read it back.
Import what you already have
Right-click an account → Import from ~/.ssh/config or another manager…
| From |
What comes across |
~/.ssh/config |
Host, HostName, User, Port, IdentityFile — as connectable SSH entries |
| Bitwarden, KeePass, LastPass, Termius (CSV) |
name, username, password, address, notes, TOTP, folder |
| Bitwarden, 1Password (JSON) |
the same, from login items |
The file's content decides how it is read, so a misnamed export still imports. Nothing lands
before you have seen the count and what will be skipped — and skipped rows are listed with the
reason, never dropped in silence. Everything gets a fresh id, so an import can never overwrite what
you already had.
KDBX (KeePass's own database) is not read, deliberately: it is Argon2-encrypted and Argon2 is
not in Node, so a half-right implementation would be worse than none. KeePass exports CSV, which
imports fine.
The health report
CredsForDevs: Health Report looks at what you have and says what is wrong with it:
- passwords used in more than one entry — the finding you cannot see by eye, and the one that
turns a single breach into several;
- passwords under 60 bits;
- private keys in
~/.ssh with no passphrase — reported as medium, because that is the normal
state of a key on a machine only you use, and a report that calls everything a catastrophe is one
nobody reads twice;
- plaintext credentials in a workspace
.env, with the creds:// reference that fixes them.
All of it runs here. The one exception is opt-in: with credSshManager.breachCheck on, the
report can ask Have I Been Pwned whether a password appears
in public breach corpora — and it asks you again, each run, before it does. What is sent is the
first five characters of the password's SHA-1: one bucket out of a million, shared by hundreds
of thousands of passwords. The whole bucket comes back and the match happens on your machine, so the
service cannot tell which password was asked about. The password never leaves.
No finding ever quotes the value that caused it — the report is a document you can paste into a bug
without pasting a password into it.
Script entities
A script is the sibling of a terminal command: the same "I will never remember this
next month" problem, one size larger.
Know where the body lives. A script and its variables are stored as entity
metadata, not in the OS keychain — like a terminal command's arguments and unlike
a password. Locally that means globalState, in plaintext; in transit and at rest on
your NAS or vault server it is inside the same AES-256-GCM envelope as everything else.
So a script is safe to sync and safe to share, and it is not the place to paste a
token: put the token in a credential entity and let the script read it from an
environment binding.
Language decides both the highlighting in the form and whether Run Script is
offered at all. Only a language with an unambiguous interpreter can run:
| Language |
Runs with |
File |
| Bash |
bash (on Windows: the one git-bash ships) |
.sh |
| PowerShell |
powershell -ExecutionPolicy Bypass -File on Windows, pwsh -File elsewhere |
.ps1 |
| Python |
python |
.py |
| JavaScript |
node |
.js |
SQL, YAML, JSON, Dockerfile and other are stored, highlighted and copyable, but
not runnable — SQL needs a database and a data format is not a program. Run
Script refuses those with the reason rather than piping them into a shell.
Variables are rows, exactly like a terminal command's arguments: a name, a value
and a tick to keep one without using it. The values never enter the script text.
They are handed to the run through the process environment, and the body reads them
by name in its own language's syntax — bash needs no change at all (${NAME} already
is that), PowerShell gets $env:NAME, Python os.environ.get('NAME', '') with the
import os added only when something was actually translated, JavaScript
process.env.NAME. The file on disk, the viewer and Copy All carry names where they
used to carry values.
Two consequences worth knowing. A script runs in a fresh terminal every time, because
VS Code can only set a terminal's environment when it is created — reusing one would run
the script with the previous entry's values. And a script can still print its own
variables: env injection stops the value leaking into the file, not into anything the
script chooses to echo, so an entry whose body does that is flagged rather than silently
trusted.
Where it runs from. The file is written into the extension's own private storage
(keys/, dir 0700, file 0600, owner-only ACL on Windows) — the same directory
materialized SSH keys use, and purged on both activate and deactivate. Nothing of it
outlives the session.
Config file entities
A config entry holds a whole configuration file — the appsettings.Development.json that must
never reach git and was being passed between developers by hand. The body is a secret like a
password: never in plain metadata, never in a share's label, never handed to an agent.
- Raw and Fields. The Raw tab is the file, highlighted as you type in its own format (JSON,
YAML, TOML, INI, .env). The Fields tab is a view over the same text: it splices one value's
span, so it structurally cannot lose your indentation or comments. A body that does not parse
is still saved — the row is marked until it does, because losing a paste is worse than holding
a draft.
- Write Config File Here… materialises the body into the workspace, refusing a path git
tracks and asking about one it does not ignore. Show What Changed is a key-level diff
against the file on disk, so a colleague's edit is readable before it is taken.
- Enable Code Access… mints a long-lived key for applications: shown once, copied to
the clipboard, and the vault keeps only a SHA-256 of it. The app reads the config through
POST /v1/config/read or creds config <key>; the key travels in CREDSFORDEVS_KEY. Revoke
Code Access… retires it. The viewer's Read this from code panel gives the exact snippet in
twenty languages — and names the file it goes into ("Program.cs, before builder.Build().").
- Sharing carries the document. A shared config arrives with its body; the code-access key
deliberately does not travel — it is minted per vault, and the recipient mints their own.
Export / Import externally
Share with… is for people on your team — it needs them to have the extension, an
account and a vault. Export / Share Externally… is for everyone else: a contractor,
a client, a colleague who has not installed anything yet.
- Right-click an entity to export it, or a folder to export its whole subtree.
Secrets travel with it — password, private key, VPN config, connection string, notes,
the attachment and the image.
- An entry marked Not for export (Edit → General) never travels this way or through
Share with…: on its own it is refused, inside a folder it is left out and named. Agents,
backup and sync are unaffected.
- You are asked for a password. The file is then sealed with the same envelope the
vault itself uses (scrypt + AES-256-GCM), so what lands on disk is ciphertext and the
password is what opens it. Choosing to export plain JSON is offered and is exactly
what it says — nothing protects it; send that over nothing you would not shout across
a room.
- Import from External… takes such a file, asks for the password if it is sealed,
and gives every imported node a new id. The sender's ids belong to the sender's
tree; colliding with your own would corrupt the next sync merge.
Sharing (Team / Shared with me / Create for…)
- Team is account-scoped: every account row carries its own Team
subtree — the people discovered on THAT account's NAS folder (owners of
vault_*.enc there; people appear after their first sync), you marked
"(you)". Two companies on two NAS folders = two separate teams that
never see each other.
- Share with… (context menu on an entity or a folder — a folder
shares every entity in its subtree, one item each, preserving the folder
chain + types on the recipient's side): pick recipients (only the sending
account's team is offered), enter a one-time share PIN — each entity
(metadata + all its secrets) is sealed with
scrypt(recipientAccountId + PIN) and appended as a plaintext-array item
into the recipient's vault envelope (their encrypted payload is untouched;
only name/kind/sender are visible). Tell them the PIN out-of-band.
- Shared with me (appears when something is pending; aggregates all your
accounts): grouped by sender. Accept (inline ✓, enter PIN) imports the
entity into the addressed account's vault (same id → re-shares update the
copy) and removes the item; Decline (inline ✗, confirmed) removes
without importing. Accept all (per sender or global) tries known PINs
on everything and asks a new PIN for the first item that resists, round by
round.
- Create Entity for… (Team context menu): author an entity in the normal
form directly for someone else, sent from the account whose Team you
clicked — after a successful share nothing remains in your own storage.
- Sender identity is signed (Ed25519, since 0.45) — see Sender
signatures below for what that proves and what it does not.
- Honest note: a share is a copy. There is no remote revoke; a
recipient who has accepted it holds it.
Share with Claude Code — an agent that uses a credential it never receives
An AI coding agent needs your server. Pasting the password into its chat puts the plaintext in
a transcript and in every log downstream of it; exporting it to a file is no better.
Right-click any entity an agent can do something with → Share with Claude Code… —
SSH hosts, scripts, terminal commands, databases, VPN tunnels, and bare credentials. A capability token is minted and a
paste-ready snippet lands on your clipboard. Give it to the agent, and it can:
node "<extension>/dist/agentCli.js" ssh <token> -- systemctl status nginx # runs it, returns stdout/stderr/exit code
node "<extension>/dist/agentCli.js" terminal <token> # asks for the interactive terminal, for you
node "<extension>/dist/agentCli.js" db <token> -- "select count(*) from orders"
node "<extension>/dist/agentCli.js" script <token> # runs your stored script, exactly as saved
node "<extension>/dist/agentCli.js" run <token> # runs your stored command, exactly as saved
node "<extension>/dist/agentCli.js" env <token> # exports the secret into new terminals; returns NAMES
node "<extension>/dist/agentCli.js" vpn-up <token> # opens the tunnel; you answer the elevation prompt
Two of these deliberately ignore whatever the agent sends: script and run execute exactly
what you saved, so no agent-authored text ever reaches an interpreter or a shell. They also require
that you have run the entry yourself once on this machine — the broker's Allow covers a token, not
a body, and a body can be replaced by a sync after you clicked it.
MongoDB is refused, on purpose. mongosh has no password environment variable and its --eval
runs in the same JavaScript interpreter that can read process.env — so a "query" could print the
password straight back. No SQL client has that channel. A capability that leaks by design is worse
than one that is absent, so this one says no.
SSH keys are excluded for a duller reason: a key means nothing except attached to a host, and the
host entry already has exec.
What the agent never gets is the secret. ssh is spawned by the extension — the half that
already holds the credential — and the password rides that child process's environment through
the same SSH_ASKPASS machinery a human Connect uses. The broker has no endpoint that returns
plaintext, and this is structural rather than a promise in a comment: no response type in the
protocol has a field a secret could ride in.
- The token is a capability, not a credential. It reaches exactly one entity, and it carries
the broker's loopback port, so the CLI dials the exact window that minted it.
- It dies with the window. Grants live in memory only; closing or reloading VS Code revokes
every one of them. That is the whole revocation story — there is nothing to expire and nothing
left on disk.
- First use asks. A modal names the entity and the exact command about to run. Afterwards
that grant runs silently, but every call — allowed, denied or refused — is a line in the
CredsForDevs: Agent Access output channel. A dismissed dialog is not remembered as a
refusal: a missed notification must not lock an agent out for the window's life.
- Bounded by construction: the loopback server binds
127.0.0.1 only, a bearer token
authorizes every call, output is capped and truncated rather than buffered without limit, a
hung command is killed at its ceiling, and eight execs may run at once — a runaway agent loop
cannot fork-bomb the machine.
- Auto-lock is not fooled by it. Your click on Allow counts as presence; the agent's calls
count as nothing, for the same reason a background sync never postponed the lock.
Honest about the boundary: an agent that can run commands on a host can do anything that host
lets it do. What this removes is the plaintext credential, not the access — the access is the
point.
Agents over MCP — creds-mcp
Share with Claude Code… (above) opens ONE entry to one agent for one task. The MCP server is
the standing version: Install the MCP Server… puts a separate creds-mcp binary on PATH and
writes the client config; the extension itself keeps its zero runtime dependencies.
- Two ladders, ten switches, all off by default. Over ENTRIES: visible / usable / may replace
the secret / may create / may delete what it created / may delete anything here. Over FOLDERS:
may create folders / may rename and move them / may delete the ones it made / may delete any.
Set them in the Agent access section of an entry or a folder; everything below a folder
inherits them until something inside gives its own answer, and an empty object is an answer —
that is how you close one branch of an open tree. The tree marks an opened entry with a pentagon
whose edges light per switch of the entry ladder. Nothing in the Trash answers, whatever its
switches say.
- An agent can never change a switch. Not on an entry, not on a folder, not by any route:
there is no field in any request it can make that carries them. Folder editing means the name,
the place and the type. A move needs the grant at both ends, because a folder passes its
answers to everything inside it — so moving one is a permission change for its contents.
- The switch is not consent. It says an agent may ask. Whether the modal appears — with
the real entry, the real command and when it was asked (Requested 2026-09-23 14:05:12
(UTC+03:00).) — is that entry's consent setting: every time, once every 12 hours, or never.
- How often it asks is yours to set, on the entry or on a folder it inherits from: ask every
time (the default), ask once every 12 hours, or never ask. It covers using an entry only —
creating and deleting always ask, whatever it says — and never-ask makes the switches the whole
gate, which the form says in those words beside the option. The twelve-hour window is remembered
on this machine alone and is never synced or shared; Forget Agent Consents on This Machine
takes every such window back at once. Every call is recorded either way, in ⋯ → MCP logs.
- The agent may shape a generated secret, never see it.
length, the four character sets and
avoidAmbiguous for a password; words and separator for a passphrase. It matters because the
alternative to "this system caps passwords at 16" is an agent generating the value itself, and
then the secret is in its context. Asking for a password with no character sets is refused rather
than drawn — the generator answers with an empty string, and an empty secret stored as if it were
one is a working-looking entry that is not.
- Rotation without disclosure: an agent writes
{{creds:new}}, the extension generates the
value, and neither the old nor the new secret ever enters the agent's context.
creds_config_snippet answers "how does code read this config?" from the same catalog the
viewer renders — languages first, then the snippet, its target file, and the CREDSFORDEVS_KEY
environment variable. The listing's codeAccessEnabled tells the agent whether the key exists;
minting one is yours alone.
- MCP logs (the
… menu) is the journal: every ask, every refusal, and the two costs worth
counting — secrets that came FROM an agent, and requests we could not serve. Show Entry by
id… jumps from a journal line to the entry it names.
- It works from inside WSL, where the agent usually lives. The window is on Windows and
127.0.0.1 inside a distribution is that virtual machine's loopback, so the Linux creds-mcp
hands the whole session to creds-mcp.exe through WSL interop and carries its stdio — nothing
new listens anywhere, and the consent modal still appears on Windows. Install the MCP
Server… asks where the agent runs and installs both halves: the Linux one into the
distribution you pick, and a config block naming it, with the Windows binary's path in env
— asked of the distribution through wslpath rather than composed here.
Help — the yellow question mark
The question mark in the view's title bar opens the built-in help: every feature as an article in
one fixed shape — what it is, why, how to set it up, how to use it, what can go wrong — with the
least self-explanatory ones first (MCP logs, Install…), a search box, breadcrumbs and Back,
and a language switch (English, Russian, Ukrainian, German, Spanish) that remembers your choice
for the help pages only. Pictures come later; the slots are there.
Selecting several at once
Ctrl-click or Shift-click in the tree, then Delete, Export / Share Externally… or
Share with… — one confirmation, one recipient pick, one PIN, one file, however many rows are
selected. Everything else still acts on the row you clicked.
Rows that cannot take part are left out and named: an account row, a team member, an inbox item.
Rows from a different profile are left out too — ctrl-clicking across two account roots is an
ordinary gesture, so it is reported rather than refused. And a folder quietly swallows anything of
its own you also selected, at any depth, because deleting or exporting the folder already covers it.
Dates and history
Every entry carries when it was created and when it last changed, shown in both the viewer
and the edit form. The creation date is stamped once and never moves again, so it survives every
later edit; entries made before this feature existed say the date is unknown rather than inventing
one.
The last 3 versions of an entry are kept. An entry with history wears a blue-tinted icon, so
"this has been changed" is visible in the tree rather than only after opening it, and the viewer
lists each kept version — when it was replaced, what it was called then, and a button to copy that
version's secret.
The versions are rows in the tree: open the entry's twisty and they are underneath, newest
first, labelled by when each was replaced. One click on a version opens the viewer on that
version — every copy button reads the old value; a version of an entry protected with its own PIN
opens only after that PIN. Right-click → Restore This Version… brings the entry back to it: what
the entry holds now becomes its newest previous version, so a restore is undone the same way, and
its agent access and code-access key stay today's. Run and Copy Command work on an old version of
a command, and Clone makes a new entry from one. Nothing else is offered on a version:
no Edit, no Share, and no writing its secret into a terminal variable. A clone never carries
history — a new id starts with an empty past, as it starts with no secrets.
Two limits, so they are not discovered the hard way: a kept version does not include attachments
(three copies of a 4 MB file per entry would cost more than the history is worth), and history is
local to the machine — it is not part of the encrypted vault that syncs, so another machine keeps
its own. And one fact worth knowing before you rely on it: history means a replaced password stays
retrievable. That is the point of it, and it is why the kept versions live in the same encrypted
store as the current ones.
When somebody re-shares the same thing
A colleague sends you the password for an account. Six months later they change it and send it
again. Rather than a second copy appearing beside the first with nothing saying which is current,
the second one asks: Update it — in place, keeping its folder, its identity and its history — or
Keep both.
Dismissing that question leaves the item in "Shared with me." Deciding usually needs a look at
what you already have, and consuming the share to ask you would destroy the only copy of the
decision. Come back to it when you know.
How it knows, and why a sender cannot abuse it: the record is local to your machine and keyed by
who sent it together with what they called it. A sender can never address an entry they never
sent you, whatever identity they claim — which is exactly the protection that made every accepted
share a new entry in the first place.
Accounts
- Add Account → Microsoft (works out of the box) or Google.
- Google: VS Code has no built-in Google provider, so the extension
registers its own (OAuth 2.0 code flow + PKCE, system browser, loopback
redirect on
127.0.0.1). One-time setup, prompted inline on first
sign-in: create a Desktop app OAuth client in
Google Cloud Console
(consent screen → External, add yourself as a test user), paste the
client id (saved to credSshManager.googleClientId) and the client
secret (SecretStorage). Reset Google OAuth clears both.
- Sign Out / Remove Account (inline icon or context menu) removes the
profile, its tree, and its secrets; for Google it also drops the auth
session (Microsoft sessions are owned by VS Code's Accounts menu).
Vault locations: NAS folder or vault server
Every account syncs to a location, set per account (account row →
Set Sync Location…, stored in credSshManager.accountNasPaths; the
global credSshManager.nasBackupPath is the fallback):
- a folder (
/mnt/v/vault, Z:\Backups, \\NAS\Vault) — the original
transport: one vault_<email>.enc per person in a shared folder, pending
shares carried inside each file's plaintext envelope array. Anyone with
folder access can read everyone's ciphertext.
- a server URL (
https://vault.company.com) — the
Cred Vault Server (cred-vault-server/ in this repository): every request carries
that account's own OAuth token, so you can read only your own vault and
inbox, and the server stamps a share's sender from the verified token
(unforgeable). Recommended for company-wide use.
Mixed setups are normal: the corporate account on the company server, the
personal one on your own NAS. Shares are bound to the recipient's account id
on folders and to their email on the server (that is the identity the
server enforces) — the transport handles this transparently.
Which transport for what (architectural boundary)
NAS folder → personal / solo sync, now with signed senders. It remains the
right choice for one person syncing their own vault across their own machines.
A share sent over a folder is signed with the sender's Ed25519 key, and the
recipient pins that key on first contact: every later share from that
address must match it, and one that does not is refused with both fingerprints
shown side by side.
What that is and is not. A signature proves "signed by the holder of key
K". Tying K to a person is a separate problem, and if keys travel over the same
folder an attacker can write, they can publish their own. So this is
trust-on-first-use plus continuity — strong against somebody who turns up
after you have exchanged a share, weak against somebody already in place before
the first one. The only thing that closes that gap is reading the fingerprint to
each other out of band, which is why the first-contact dialog shows it and why
Show Signing Fingerprint… exists on your own account row. It is
forgery-resistant, not forgery-proof, and should never be described as the
latter.
Since 0.82 a share's label — sender, name, kind, date — is bound to its ciphertext as
GCM additional authenticated data: a label edited after sealing breaks decryption instead of
changing what you are shown. A share from an older build is unbound, says so in the inbox and
on the PIN prompt, and opens until 0.85.0; from then on it is refused with a request to update
the sender.
A share from an older build carries no signature and is shown as unsigned rather
than refused. A sender who has signed before and suddenly does not is a
different matter and is refused: that is what stripping a signature looks like.
Server transport → the recommended standard for teams. The Cred Vault
Server authenticates every request with that account's OAuth token and
stamps the share sender from the verified token, so fromEmail is
unforgeable. Any deployment where people share credentials with each other
should use the server, not a shared NAS.
Unlocking with a security key (YubiKey / FIDO2)
A vault can be opened by several security keys plus the PIN, in the
"touch your key" style of a Microsoft sign-in — no typed password.
- Add: account row → Add Security Key (YubiKey)…. The browser opens a
local
http://creds-for-devs.localhost page (loopback by RFC 6761 — a name no
other local page can claim), the OS shows its native security-key prompt,
and the key's WebAuthn PRF secret becomes a wrapping key.
- How it is stored: a v2 vault encrypts its payload with a random master
key, and that master key is wrapped once per unlock method — the PIN wrap
and one wrap per registered key — as plaintext metadata in the envelope
(
wraps[]). Any wrap opens the vault, so adding or removing a key never
re-encrypts your data and never invalidates the others.
- Every vault is v3 now — PIN-only included — so update every machine BEFORE anyone syncs.
A vault used to stay on the slow v1 format (PIN-derived key, scrypt on every read and write)
unless you registered a security key. As of this release nothing writes v1 any more: the next
save migrates a PIN-only vault to v3 (a random master key, wrapped once under your PIN, read
with HKDF), a brand-new vault is v3 from the start, and your backups convert on their next
run too. The migration is automatic and keeps the same PIN — nothing is converted by hand and
no data is touched. The catch is the same as any format bump: a build older than this one refuses
a v3 file ("Unsupported backup version: 3"), so one updated laptop syncing to a shared folder
can lock a colleague still on an old build out of their own vault. Roll the extension out to
everyone first, then sync. Reading a legacy v1/v2 file keeps working forever.
- Unlock: the master key is cached in memory for the window, so
background sync never asks for a touch again.
Lock Vaults (clear cached keys) drops the cache; Unlock Vault (Security Key)… prompts on demand.
- Auto-lock (
credSshManager.autoLockMinutes, default 60, 0 disables) locks the
vaults after an idle window measured in your actions — opening or copying a
credential, connecting, installing a key, editing an entry, unlocking. Not mouse
movement, and deliberately not background sync: a five-minute sync timer counted as
activity would mean the window never elapses and the setting silently does nothing.
Locking forgets the cached master key; local credentials keep working, because they
live in the OS keychain and are not protected by the vault key.
- Remove: account row → Remove Security Key… (pick from the list).
- The PIN always remains a fallback — losing every key does not lock you
out, and losing the PIN does not either as long as one key is registered.
- Requirements & limits: PRF needs Chrome or Edge as the default browser
and a FIDO2 key with hmac-secret (YubiKey 5 or newer). Firefox/Safari
currently return no PRF result — the flow reports that and the PIN keeps
working. Credentials are registered against the RP id
creds-for-devs.localhost
(0.81+), so the same physical key works on every machine — and no other local
page can ask the key for this vault's secret. A key registered before 0.81 is
bound to the bare localhost: it keeps unlocking, and the first time it does
you are offered a one-touch re-registration; the old registration is retired
only once the new one is in the vault, and the PIN works throughout.
Recovery — the printed code, and the corporate break-glass
- Set Up Recovery Code… shows a 150-bit code once, with Print and deliberately no Copy;
Unlock Vault (Recovery Code)… opens the vault with it and offers a new PIN; Remove
Recovery Code… retires it.
- Corporate Recovery… is the operator's panel on a self-hosted server: name the officers,
set the quorum. Accept Recovery Share… is each officer taking their share (per machine —
shares live in this machine's keychain); Recover a Colleague's Vault… starts the
break-glass, Contribute to a Recovery… is an officer's half, Finish a Recovery…
re-keys the vault under a temporary PIN told to the target out of band.
Corporate roles — admin, member, dev
On a server with recovery officers, every account has a role, and the server tells the
extension which on every sync — together with the policy for that role (may export, may share
with whom, may move an entry out of a project). Set Role… on a colleague's Team row lets an
admin — or a recovery officer, who administers unconditionally — make somebody admin, member
(today's behaviour, and the default) or dev, and for a dev choose whether they may share inside
their projects or not at all. My Role and Policy… on an account row shows what the server
says about you.
The event log. A corporate server records what happened — shares with their outcome (taken,
declined, withdrawn, expired), roles, blocks, projects and assignments — and Event Log… on an
account row opens it as a tab: newest first, four buttons to narrow by, Load more for the page
before. The server decides what each person sees: an admin — or a recovery officer, who administers
unconditionally — reads the company, everybody else reads only the rows naming them. It records metadata — an entry's name and kind — and never a byte of what
was in it.
Server Backup. Server Backup… on the account row of an admin — or of a recovery officer, who administers unconditionally — takes one encrypted archive of
everything the server holds — every vault, the sealed login keys, the registry, projects, the event
log and a snapshot of the server's own configuration — on a schedule, and sends it to an
S3-compatible bucket or an Azure Blob container once a destination is configured. Minting the backup
key shows its words once: nothing can produce them again, and without them no archive can be
opened. The tab shows the last run, the newest archive on the server, and a row per destination
saying whether the upload arrived and whether the retention pass could then run; Back up now
starts one immediately and Download newest archive streams it to a file you choose. A deployment
with no key, or whose last run failed, is said out loud — once a day, or once an hour while failing —
because the failure this exists to end is a scheduler refusing every five minutes with nobody
watching.
Projects. An admin runs New Project… from Team — that is where the first one is made —
then Assign to Project… and Remove from Project… on a colleague's row. An assignment
becomes a folder in that person's own vault, named after the project and following its name; for a
developer that folder cannot be renamed, moved or deleted, and an entry cannot be moved out of it
except to the Trash. Removing somebody asks what happens to their copy — leave it with them, or
delete it from every machine they sync — because the two are not recoverable from each other. A
Team row shows the projects a colleague is on beside their role.
A developer's sharing is the one rule the server enforces rather than asking an honest client
to obey: they may share only out of a project folder, only with somebody on that project, and only
while the project is open. The project travels with the share and is bound into its encryption, so
it cannot be edited afterwards.
NAS backup & automatic multi-PC sync
Everything travels as one AES-256-GCM encrypted file per profile
(vault_<sanitized email>.enc) in the folder set by
credSshManager.nasBackupPath. The key is profile-bound:
scryptSync(accountId + PIN, salt, 32) — restoring needs both an active
auth session for that account and the PIN. Filenames are collision-safe
(same email under two providers gets distinct names). The envelope keeps
account metadata in plaintext (needed to verify the session and derive
the key before decryption); salt/IV/GCM-tag are public by design; the
payload (tree, passwords, private keys, VPN configs, DB connection
strings, tombstones) is ciphertext.
The PIN is the only protection — use a real passphrase, not 4 digits.
- Manual:
Backup to NAS / Import / Restore (also reachable as the
original spec's command ids extension.exportSecrets /
extension.importSecrets). These prompt for the PIN every time.
- Automatic (
credSshManager.autoSync): runs ~5s after every change,
on startup, and every autoSyncIntervalMinutes (default 5); manual
trigger via the toolbar's Sync Now. The sync PIN is per account,
per machine (Set Sync PIN asks which account; a previously set
machine-wide PIN keeps working as the fallback) and must match across
machines for that account.
- Per-account folders: each account can sync to its own NAS — account
row → Set Sync Folder… (stored in
credSshManager.accountNasPaths,
email → path; unmapped accounts use the global nasBackupPath). The
corporate account lives on the company NAS, the personal one on yours;
teams, shares, and backups all follow the account's folder.
- Merge, not overwrite (causal version vectors): both PCs may change
data. Each machine has a persistent
deviceId and a monotonic counter;
every node carries a version vector v ({deviceId: seq}) alongside
updatedAt. On merge, the vector that causally dominates wins — so a
later edit beats an earlier one even when a skewed clock disagrees. Truly
concurrent edits (neither vector dominates) fall back to the higher
updatedAt, then to the lexicographically-greater last-writer deviceId,
so both machines converge to the same winner regardless of merge order.
- Deletions and rollback protection: deletions leave lightweight
tombstones (
{deletedAt, v}, no secret payload); an edit whose vector is
causally newer than the delete resurrects the node, otherwise the deletion
wins. A per-profile horizon (element-wise max of every vector ever
seen, never pruned) lets tombstones be hard-deleted after 90 days without
losing the causal memory: a node restored from a stale backup whose vector
the horizon already covers is rejected as a phantom instead of resurrecting.
Legacy pre-vector vaults (no v) still merge by updatedAt/tombstone time
and adopt a vector on their first write. Secrets follow the winning side;
orphaned children re-parent to root. NAS writes are temp-file + atomic
rename; a file that fails to decrypt (wrong PIN / corrupted) is reported
once and never overwritten — mismatched PINs pause sync, they cannot
destroy data.
The creds CLI, bridges and WSL — the commands
- Enable CLI Access… on an entry mints a named grant:
creds ssh prod-db instead of a
pasted token. The entry's viewer lists its CLI aliases with the exact command ready to copy.
- Install
creds (terminal CLI)… installs the binary locally; Copy install command for
another machine... puts a one-line installer on the clipboard; Install creds on the
Host… installs it on a Remote-SSH host from the entity's own menu.
- Open Remote Bridge… / Close Remote Bridge hold the
ssh -R tunnel that lets the
host's creds reach this window; the menu item says which state it is in.
- Set Up the WSL Agent Relay turns on the agent relay inside your distributions
(
wslAgentRelay, wslRelayCommand, wslRelayDistros are its settings — which distros, and
what the relay runs).
Per-machine setup (WSL example)
# make the NAS/drive visible inside WSL (network drives are not auto-mounted)
wsl.exe -u root -e sh -c 'mkdir -p /mnt/v && mount -t drvfs V: /mnt/v'
# persist: echo 'V: /mnt/v drvfs defaults,uid=1000,gid=1000,metadata 0 0' >> /etc/fstab
Then set credSshManager.nasBackupPath (e.g. /mnt/v/vs code extn passw manager), enable credSshManager.autoSync, run Set Sync PIN with the
shared PIN, and sign into the same account profile.
Dated snapshots (separate from sync, deliberately)
Sync keeps one live vault and merges — which means a deletion propagates. Delete a
credential on the laptop and the desktop's next sync agrees it is gone. That is correct for a
live vault and useless as a safety net, so snapshots are a second, independent path:
- Where:
credSshManager.backupLocation, or per account via account row → Set Backup
Location… (accountBackupPaths). A NAS folder, an external drive, a Google Drive or
OneDrive sync folder. Empty disables it.
- When:
backupIntervalHours (24 daily, 168 weekly, 0 off), or per account via Set
Backup Schedule…. Snapshot Vault Now takes one on demand.
- A snapshot identical to the previous one is not written, so a quiet vault does not fill a
metered folder with copies of itself.
- Retention:
backupRetainDays deletes older ones (0 keeps everything). The newest is
never deleted whatever its age — a laptop closed for a year must not come back to an empty
backup folder.
Snapshots are the same encrypted format as everything else: they open with the account and the
PIN, and carry attachments, images and VPN configs like passwords.
Settings
Almost nothing here needs editing by hand. Every setting that belongs to one account has a
right-click command on the account row, and that is the intended way in — the entries below say
which one. The raw keys are documented because a settings.json is what an admin pushes to a
fleet, and because a value you cannot find is a value you cannot trust.
Two rules govern how they combine:
- Per-account beats global.
accountNasPaths / accountBackupPaths /
accountBackupIntervals map an email to a value; an account with no mapping falls back to
the global nasBackupPath / backupLocation / backupIntervalHours. That is what lets a
work profile on a company server and a personal profile on a home NAS live in one window.
- Nothing here holds a secret. Locations, intervals, client ids and scopes — all of it is
safe in a synced settings.json or a fleet policy. Every actual secret is in the OS keychain.
Where the vault lives
| Setting |
Default |
What it does |
nasBackupPath |
(empty) |
The default vault location for accounts with no mapping of their own: either a folder for the encrypted vault_<email>.enc files (/mnt/z/Backups, Z:\Backups, \\NAS\Vault) or a Cred Vault Server URL (https://vault.company.com). A URL switches that account to authenticated server sync — the two transports differ in more than spelling, see Which transport for what above |
accountNasPaths |
{} |
Per-account override: { "work@corp.com": "https://vault.corp.com", "me@gmail.com": "/mnt/home-nas/vault" }. From the UI: account right-click → Set Sync Location… |
autoSync |
false |
Sync every profile automatically — after each change, at startup, and on the interval below. Edits from several machines are merged per node, not overwritten. Needs the same Sync PIN on every machine (Set Sync PIN) |
autoSyncIntervalMinutes |
5 |
How often auto-sync pulls. Lower it on a fast LAN, raise it on a metered link |
Snapshots — the safety net, deliberately not the same thing as sync
| Setting |
Default |
What it does |
backupLocation |
(empty — off) |
Where dated snapshots are written. Point it at different storage from nasBackupPath: the sync location merges, so a deletion travels to every machine; the snapshot is the copy that still has what you deleted. Same encrypted bytes, no PIN needed to take one, restored with Import / Restore |
accountBackupPaths |
{} |
Per-account snapshot folder. From the UI: account right-click → Set Backup Location… |
backupIntervalHours |
24 |
1 hourly · 24 daily · 168 weekly · 0 off. A snapshot identical to the previous one is not written, so a quiet vault does not fill a metered folder with copies of itself |
accountBackupIntervals |
{} |
Per-account schedule, in hours. From the UI: account right-click → Set Backup Schedule… (hourly / 6h / daily / weekly / off / custom) |
backupRetainDays |
30 |
Delete snapshots older than this; 0 keeps them forever. The newest is never deleted whatever its age — a laptop closed for a year must not come back to an empty backup folder |
Agents, the CLI and WSL
| Setting |
Default |
What it does |
agentGrantIdleMinutes |
60 |
A Share with Claude Code token dies after this long unused. 0 disables the idle timeout — the token still dies with the window |
agentGrantMaxCalls |
0 |
The most calls one token may make before it expires; 0 is no cap. Useful for a one-off task: share, let the agent make its handful of calls, and know the token is spent |
wslAgentRelay |
false |
Serve the SSH agent on a unix socket inside WSL distributions, so ssh and git there use vault keys with a dialog per signature. From the UI: Set Up the WSL Agent Relay |
wslRelayCommand |
(auto) |
What the relay runs inside the distribution — override only if creds is not on the default path there |
wslRelayDistros |
[] |
Which distributions get the relay; empty means the default distribution |
gitDeployKeys |
{} |
Per-repository SSH key for the git sync transport: maps a repo URL to the entity whose key pushes it |
Everything else
| Setting |
Default |
What it does |
helpLanguage |
en |
The language of the Help pages only (en, ru, uk, de, es); articles not yet translated show English with a note. The switch at the top of the help page writes this same setting |
uiScale |
0 |
Text size on every CredsForDevs page, in steps of 10% (−5…5). The ± buttons on each page write this same setting, and it syncs |
secretClipboardTtlSeconds |
45 |
How long a copied secret stays on the clipboard before the extension clears it — if the clipboard still holds exactly what was copied |
microsoftApiScope |
(default scope) |
The OAuth scope requested for the vault server sign-in; set it when your server's app registration names its own |
Locking
| Setting |
Default |
What it does |
autoLockMinutes |
60 |
Lock after this many minutes without you using the vault; 0 disables. "Using" means an action of yours that touches a secret — open, copy, connect, install a key, edit, unlock. It is not mouse movement and not background sync: a timer firing is not you being present. Locking forgets the cached master key and refuses the saved Sync PIN until you unlock deliberately; your credentials keep working locally, because they live in the OS keychain and are not protected by the vault key. From the UI: Set Auto-Lock… |
Sign-in
| Setting |
Default |
What it does |
microsoftApiScope |
(empty) |
The API scope of your own Entra app registration, e.g. api://<client-id>/vault.access. Against server 0.2.3 and newer, leave this empty — the server publishes the value on /api/client-config and the extension asks for the right scope by itself. Set it only to override a server advertising the wrong value, or to work against an older server. Why it exists: with no scope the extension receives a Microsoft Graph token, and Graph tokens are deliberately unverifiable by third parties, so every server refuses them with 401 — see When the Team is empty below |
googleClientId |
(empty) |
OAuth 2.0 client id of your Desktop app credential (Google Cloud Console → APIs & Services → Credentials). Required for Sign in with Google. The client secret is prompted once and kept in SecretStorage, never in settings |
Behaviour
| Setting |
Default |
What it does |
dbExtensions |
{} |
Which extension Open in DB Extension hands a connection to, per DB type: { "mysql": "cweijan.vscode-mysql-client2" }. Empty = the first installed candidate wins |
secretClipboardTtlSeconds |
45 |
How long a copied secret stays on the clipboard before it is cleared — and only if the clipboard still holds exactly what was copied, so a later copy of your own is never destroyed. What no extension can control: Windows Clipboard History (Win+V) and cross-device sync capture the value the moment it is copied, and clearing the clipboard afterwards does not reach them. Turn those off if you copy secrets on a machine you do not control |
readCliHelp |
true |
When you paste a whole command into a terminal entry, fill the empty notes by running <tool> --help. It runs the tool you just typed, with no shell and no arguments of ours, and only when every word of the command is a plain tool name — anything containing a shell metacharacter is never run. It never overwrites a note you wrote. Turn it off if you would rather nothing were executed while you edit |
When the Team is empty
The one failure this product used to produce on its own — everyone signed in, sync green, no
error, and nobody in each other's Team. Three things now stand between you and it:
- 0.46 stopped swallowing the server's refusal. A 401/403 when listing the team is shown
with the reason and what to do about it, instead of returning an empty list that looks
exactly like a team nobody has joined yet.
- Server 0.2.3 makes it not happen: the server advertises its scope, the extension
configures itself, and a developer signs in and is done.
- On an older server, set
microsoftApiScope to match that server's MS_AUDIENCES.
Any other cause — a domain missing from the server's allow-list, a token for a different
audience — the message names as such.
Commands
Every command lives under the CredsForDevs: category in the palette, and each one is
also on the right-click menu where it applies.
- Accounts — Add Account · Sign Out / Remove Account · Set Sync Location… · Set Backup
Location… · Set Backup Schedule… · Show Signing Fingerprint… · Reset Google OAuth
- Vault — Set Sync PIN · Add Security Key (YubiKey)… · Remove Security Key… · Unlock Vault
(Security Key)… · Lock Vaults (clear cached keys) · Set Auto-Lock…
- Tree — Add Folder · Add Entity · Edit · Clone… · Delete · Move to Folder… · Change Folder
Type… · Move Up · Move Down · View Details · Restore This Version… · Refresh
- SSH — Connect SSH · Toggle SSH (on/off) · Copy Password · Install SSH Key to System
(~/.ssh) · Add to SSH Agent · Remove from SSH Agent · Copy Git Signing Config
- One-time codes — Copy One-Time Code
- Websites — Open Site in Browser (an entry's URL, http and https only; also a button beside the
URL in View Details)
- Secrets in a run — Run with Secrets (
creds:// references, output masked)
- Getting started and getting around — Go to Credential… (
Ctrl+Alt+P) · Generate Password or
Passphrase… · Health Report · Import from ~/.ssh/config or another manager…
- VPN — Start VPN · Stop VPN · Save VPN Config As…
- Databases — Open in DB Extension · Copy Connection String
- Terminal commands — Run in Terminal · Copy Command · Show Command and Notes
- Scripts — Run Script
- Agents — Share with Claude Code…
- Sharing — Share with… · Create Entity for… · Accept… · Decline · Accept All from Sender… ·
Accept All Shared…
- Outside the team — Export / Share Externally… · Import from External…
- Sync & backup — Sync Now (NAS) · Backup to NAS · Import / Restore · Snapshot Vault Now
(the original spec's Export Secrets (backup to NAS) / Import Secrets (restore backup) —
extension.exportSecrets / extension.importSecrets — still work)
- Find and filter — Filter Credentials… (with the
has: / mcp: capability filters) ·
Clear Filter (reveals and briefly tints the row you had selected) · Show Entry by id… · Go to the Original Folder
- Config entries — Write Config File Here… · Show What Changed · Enable Code Access… ·
Revoke Code Access…
- CLI, bridge and WSL — Enable CLI Access… · Install
creds (terminal CLI)… ·
Copy install command for another machine... · Install creds on the Host… · Open Remote Bridge… · Close
Remote Bridge · Set Up the WSL Agent Relay
- Agents (MCP) — Install the MCP Server… · MCP logs
- Recovery — Set Up Recovery Code… · Unlock Vault (Recovery Code)… · Remove Recovery Code… ·
Corporate Recovery… · Accept Recovery Share… · Recover a Colleague’s Vault… · Contribute to a
Recovery… · Finish a Recovery…
- Corporate roles — Set Role… (on a colleague's Team row, for admins and officers) · My Role
and Policy… (on any account row of a corporate server)
- Corporate projects — New Project… (on the Team row, or a colleague's) · Assign to Project… ·
Remove from Project… (both on a colleague's Team row, for admins and officers)
- Keys on disk — Install SSH Key to System (~/.ssh) · Remove Installed Key… · Add to SSH Agent (confirm every use) · Remove from SSH Agent
- Scans — Health Report (weak, reused, exposed) · Check Clipboard for Vault Secrets ·
Scan This File for Vault Secrets · Show Diagnostics
- Sharing, cont. — Withdraw a Share You Sent…
- Databases, cont. — Copy Connection String (no password)
- Secrets in a run — Run with Secrets (creds:// references, output masked)
- Short-lived entries — Burn Now… (only on an entry that carries a lifetime; not the Trash:
the secret, its history and every synced copy are gone for good, after a modal that says so)
- The server, for recovery officers — Server Metrics… (on an officer's account row: the
server's version and runtime support window, uptime, requests by outcome, vault traffic, what the
data directory holds and the free space on its disk — one page, read from
GET /api/metrics, which
answers the recovery roster and nobody else)
- The Trash — Restore (first on a deleted entry's or folder's menu: back to where it was deleted
from, or the root when that folder is gone) · Empty the Trash Now ·
Empty the Trash Automatically… (each account's own retention, travelling with the vault)
Security notes
A full audit of every place a secret could be read without opening the vault ran in 0.50, and
each finding is closed or bounded below. One of them had been introduced two releases earlier
by the same hand that found it; it is listed rather than quietly patched, because the
invariant it broke is written down and a broken invariant that leaves no trace breaks again.
- A script's variables never enter the script text. They travel in the process
environment, and the body reads them by name in its own language. So the file on disk, the
viewer, and Copy All carry names where they used to carry values. A script that prints its
own variable is still your code — that is noticed and mentioned once, not blocked.
- The env-variable check button reports presence and length, never the value. It used to
echo
NAME=value, which put a bound private key into terminal scrollback in full.
- File permissions on Windows are real now.
chmod 0600 is nearly a no-op there, and the
inherited NTFS list gives SYSTEM and the local Administrators group full control of
everything under your profile — the wrong audience precisely on a machine where you are not
the administrator. Every file this extension writes a secret into now breaks that
inheritance and grants its owner alone.
- Installing a key into
~/.ssh says that the copy is permanent, and Remove Installed
Key… is the way back.
- Copying a DB connection string says the password is in it, and Copy Connection String
(no password) is the companion for when it should not be.
- Passwords, private keys, VPN configs, and DB connection strings live
only in SecretStorage and inside the encrypted
.enc files; they are
never written to globalState, settings, or logs, and never sent into
webview HTML. (Tree metadata — host/user/port/notes/public key — is
globalState plaintext by design; don't put secrets in notes.)
- PIN strength is enforced (min 8 chars) everywhere a PIN is set — it is
the sole barrier on ciphertext that lives off your machine.
- Removing the last security key re-keys the vault under your PIN, so a
removed YubiKey (and stale backups holding its wrap) can no longer open
future versions. With other keys still registered, removal drops that
wrap but does not re-key existing copies (you're told so).
- Accepting a share always creates a fresh local entity — a sender can
never address, and thus silently overwrite, something already in your vault.
- An AI agent granted access never receives the secret. It holds a token that buys
one entity's worth of work;
ssh is run by the extension, the password rides that
child process's environment, and no response the broker can send has a field a secret
could travel in. The grant dies with the VS Code window, its first use needs your click,
and every call is written down. What it does not remove is the access itself — an agent
that can run commands on a host can do what that host allows.
- Decrypted SSH keys are ephemeral: materialized only for an active
ssh -i session (0600), deleted when that terminal closes and purged on every
activate/deactivate — they never accumulate on disk.
- Server sync must be HTTPS: a plain-
http:// server URL (except
localhost) triggers a modal warning before use, since the bearer token
would otherwise travel in clear.
- The WebAuthn unlock page is served only at an unguessable loopback path, so
another local process can't read its challenge/nonce.
- KDF cost is versioned in the header: new data is sealed at scrypt
N=2^17 (params recorded per blob); older data reads at N=2^15 and upgrades
to the higher cost the next time its vault is written.
- Cross-machine merge is causal (version vectors + a per-profile horizon),
so a stale/rolled-back backup can't resurrect a deleted entry even after its
tombstone is GC'd — see the sync section above.
- On a shared folder, the envelope's integrity check covers your own
metadata but not the cross-user share list, because any member has to be able
to append to it. Share metadata on a NAS is therefore not forgery-proof by
cryptography — it is by deployment: teams use the server transport, which
stamps the sender from the verified sign-in token. The folder transport is for
one person on several machines.
- Backups decrypt only with the exact account + PIN pair; there is no
recovery for a lost PIN.
- "Copy" actions put plaintext on the system clipboard — clear it after
use on shared machines.
- A Google "Desktop app" client secret is not confidential by Google's
definition, but it is still kept in SecretStorage, not in settings.
The server, and raising it in Docker
Everything above works with no server at all — a folder is enough for one person on
several machines. The server is what makes a team work: it authenticates every request
against Microsoft Entra or Google, and stamps the sender of a shared credential from that
verified identity, so a share cannot be forged by anyone with write access to a folder.
It is zero-trust by construction: the server stores ciphertext and never holds a key.
Encryption and decryption happen in your editor, under your PIN or your security key.
Whoever runs the box — including you — cannot read what is in the vault. That is a property
of the design, not a policy anyone has to keep.
Raising it is three commands on any machine with Docker:
git clone https://github.com/oleksandrdubyna88/dew_flow_creds_for_devs
cd dew_flow_creds_for_devs/deploy
cp .env.example .env
$EDITOR .env # who may sign in, and your domain or public IP
docker compose up -d
That starts the API, an nginx in front of it, and a certbot that obtains a Let's Encrypt
certificate and then renews it forever — by domain or by bare IP, whichever you have.
Then point the extension at https://your-host with Set Sync Location… and sign in.
The image is prebuilt and published for amd64 and arm64 — a ~50 MB Native-AOT build with no
shell and no .NET inside, so nothing is compiled on your server and there is very little in the
container to attack. Vault data, logs and certificates live in host folders you choose, so
updating the image never touches them. Prefer no Docker at all? Every release also ships
standalone binaries for Linux and Windows, x64 and ARM64 — one file, no runtime to install.
deploy/README.md
covers the rest: sign-in providers, TLS by IP against by domain, scheduled backups to a NAS,
restore rehearsal, and one-command updates.
Source, issues, licence
| |