Named entries
Variables
Named entries. A variable is a snippet you can paste, a value a rule can read, a list you can deal out one paste at a time — and, where it needs to be, a secret the Secure Enclave keeps.
A variable is a named entry: a string under a string name, with the history of what that string used to be. Variables persist when the app quits, are never pruned by retention, and are deleted only when you delete them.
They are made in Preferences → Variables, or from the overlay with ⇧↩, and deleted in that pane or from the overlay with ⌘⌫. They are available to regexes, to scripts and to pipelines as $name, and they can be searched and pasted straight from the overlay — which makes them double as snippets.
$signature paste what it holds
$1 | trim > $tidy put something in it
$greeting ++ ", " ++ $name read two in one command
${name} is the same thing as $name; the braces are there for a name the text around it would otherwise run into. A variable's name may not be a whole number, because those are the clipboard positions $1, $2 … in the search bar. Only the current value is what ${name} and PD_VAR_name resolve to; the past ones are there to be looked at and restored.
Lists
A variable that a command has put several things into is a list, addressable as @name as well as $name. @shopping is the items; $shopping is the text they make, one item to a line; @shopping[0] is the first of them, and @shopping[-1] the last. Pipelines → Lists has what the filters make of one and how it is dealt out, and one item has the brackets in full — including the one thing worth knowing before you use them, which is that they count from zero while the item filter counts from one; here is what one is, and how one is built:
Being a list belongs to the value rather than to the name, so an undo reaches a version that was one and a version that was not alike. Typing over a list's value in the pane is typing over its text, and its items become that text's lines — being a list is what the name @foo means, and typing is no way to say otherwise. Regexes and scripts see the text, one item to a line, since neither deals in anything else.
Building one
⌘↩ in the overlay gathers the highlighted row into a list, and writes the list itself into the search bar:
@<$2, >
↑ the caret, inside the brackets, where the next item goes
Press it on another row and that one is written in beside the first — @<$2, $5, > — so a list is built by walking the overlay and pressing one key, and ⌘1–⌘9 puts a row in as well. While that shape is in the bar the rows underneath are the clipboard history, since what is being asked for is the next item. ↩ runs it, which puts the list on the clipboard, dealt out an item per paste; typing > @name on the end keeps it under a name instead.
The same list can be typed out from the front of the search bar. @<…> holds its elements as they were typed and expands them when the command runs, so @<$1, $2> names two things rather than spelling them. Only commas at the top level divide it, and a trailing comma is room left for the next item rather than an empty one — which is what makes @<a, > a list of one, and @<> the empty list.
>> adds to a list that is already there, and makes one out of a name that is not one yet:
$1 >> @shopping the newest copy, added to the end
$1 | split , >> @shopping each of its items
$1 | split , > @shopping the list becomes those items
What goes in is what the pipeline produced: one item for plain text, however many lines it holds, and each item where the result was itself a list — split is how several are added at once. An add keeps what was there in the entry's history like any other change to a variable, so @shopping | undo 1 is the list as it was before the last thing was added.
The Variables pane
Shows the variables you have, their values, how many items a list holds and how many values each has held. The sigil in front of the name — $, @ or $! — is shown rather than typed: what makes an entry a list is a command having put several things in it, and a sigil that could be edited would be a third way of saying so.
A list shows its value as a drop-down rather than a field: the row says how many items and the first of them, and opens to a field per item, numbered the way item 2 names them — from zero, so the number beside a field is the one you would type. Items can be added and removed there as well as changed, which makes the drop-down the way to correct a list by hand; the commands — >, >>, split, ⌘↩ in the overlay — are the way to build one in the first place.
Selecting a variable lists its past values below the table — value, when it was set and what set it — each with a Restore button that makes it current again, plus a way to forget the past values and keep only the current one. Typing a new value is one change however many keystrokes it takes; typing over a value that a trigger or an action produced always keeps that produced value in the history.
Duplicate names are flagged and the topmost wins. A name that is a whole number is flagged as an error.
Secrets
A secret is a variable whose name begins with ! — $!token, $!aws-key. The ! is the first character and is part of the name: the variable is called !token, and nothing anywhere carries the mark separately.
Three things are true of a secret and of nothing else.
Its value is encrypted at rest. config.json holds ciphertext, and so does the entry in memory — a secret's value is the empty string everywhere outside the vault, which is what stops a preview, a row, a HUD or a log line from showing one by accident.
It is not searchable. Not by value, which is the point, and not by name either: somebody typing aws is not told by the list that there is an $!aws-key. Typing $! in the search bar is the only road to one. $! alone lists them all, alphabetically; @! offers them too, since @!name reads a secret by its lines.
Reading it needs you to be there — Touch ID, or the login password on a Mac without it.
Everything else about a secret is a variable. It keeps the history of what it used to be, sealed the same way, and it is deleted like one. Being a list is the one thing a secret cannot be recorded as: what is sealed is one run of characters.
Making one
A secret is made deliberately, from the + menu in the Variables pane, or by turning an existing variable into one with Make Secret. Typing a ! at the front of a variable's name does not make it a secret — a variable that could become one by a keystroke would become one with everything it already held still written down in the clear.
Make Secret drops the past values. They were written to config.json in the clear and have been on the disk ever since; sealing a copy of the newest one now would encrypt the present and make a promise about the past that it cannot keep.
There is no way back. A secret cannot be turned into a plain variable, because doing so would write it to disk in the clear; reveal it and make a new variable, or delete it.
In the pane a secret shows the mask in its Value column, with a drop-down holding the two things anybody wants to do with one: Reveal, which reads it and is the half that costs an unlock, and a secure field to set a new value, which costs nothing. Neither what was read nor what was typed outlives the drop-down being closed.
The vault
The vault is a P-256 key pair whose private half lives in the Secure Enclave, under an access control that demands you be present. The public half sits beside it in a file, in the open, because it is public. That asymmetry is the design rather than an implementation detail:
- Sealing needs only the public key, so writing a secret never prompts. A command can store one while the vault is shut, the pane can set one without a fingerprint, and the value is encrypted from the moment it exists. There is no window in which a new secret sits in memory or on disk in the clear waiting for somebody to authenticate.
- Opening needs the enclave, and the enclave will not act until somebody has proved they are there.
Each value is sealed against a fresh one-off key pair, whose public half is stored beside the ciphertext; the shared secret those two derive is stretched through HKDF-SHA256 into the AES-256-GCM key that actually seals. So the same secret sealed twice is two different runs of bytes.
The private key never leaves the enclave. What is on disk — vault.key — is an opaque blob the enclave wrapped, useless to any other Mac, so a config carried to a different machine cannot be opened and says so rather than appearing empty. On the one or two Macs that have no Secure Enclave the private key is a software P-256 key in the login keychain instead: the shape is identical and the guarantee is weaker, the presence check being the app's rather than the hardware's.
One unlock, and how long it lasts
One unlock covers every secret until the window closes. The window is 5 minutes, 1 hour or 24 hours, chosen in General preferences, and 5 minutes is the default — long enough to write the command that uses the secret, short enough that a Mac left unattended is shut again by the time anybody reaches it. Sleeping or locking the screen shuts it early, whatever the clock says.
Writing a secret never asks. > $!token seals and stores while the vault is shut; so does setting one in the pane, and so does Make Secret. Only reading is gated, because only reading needs the enclave.
Reading one while the vault is shut
A pipeline is evaluated off the main thread and has to stay pure — it cannot go and put a prompt on the screen in the middle of a filter chain. So the secrets travel into a run as a snapshot, and a run can be in one of three states:
- Unlocked — the plain text is in the snapshot and resolves normally.
- Masked — every secret reads as
••••••••. This is what the preview above the search bar uses, redrawn on every keystroke: anything else would either show a secret on the way to typing it or raise a Touch ID prompt at typing speed. - Locked — the run stops where it stands, writes nothing, and comes back saying which secret it wanted. The app then opens the vault and runs the whole command again, which costs nothing, because a run that stopped this way did nothing.
A shut vault is not an error. $!token on a locked vault is a right command waiting on a fingerprint; $!nope is still "No variable named …", and the two are told apart rather than run together. The retry happens once: a command that still cannot read its secret after the vault has been opened has a real problem — a value sealed by a different Mac — and says so instead of asking again.
In patterns, scripts and triggers
A pattern and a script reach variables by name rather than through a reference the engine can look at, so a secret they need is noticed before either runs: a script that ran with the name resolving to nothing would go on and do the wrong thing quietly.
A script receives a secret as PD_SECRET_<name>, without the ! — !token arrives as PD_SECRET_token. It has a prefix of its own partly because ! is no name for a shell variable and mostly because the prefix says in the script what the value is. Only unlocked secrets are exported at all.
A trigger cannot write a secret, and never prompts. Triggers run on a copy, with nobody necessarily at the Mac, and a Touch ID dialog appearing because something was copied would be a worse surprise than the trigger not firing. A trigger that names a locked secret leaves the ${!token} as written — a visible failure rather than a silent one.
The variable map the panes use, for telling you whether a pattern compiles and for the preview beside a trigger, reads every secret as the mask. Neither of those is a run, and a preview that showed the real value would put every secret on the screen of anybody who opened Preferences.
What never leaves this Mac
- Secrets are not in the API. A variable whose name begins with
!is not listed, not readable, not writable and not deletable through it, and a request for one is answered exactly as a request for a name that does not exist. The same is true over MCP. - Secrets never travel in an export. They are listed in the export window as left out, and the summary says how many.
- Deleting
vault.keyandvault.pubmakes every stored secret permanently unreadable.