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 value under a name — usually text, but it can be a picture, formatted text or files — with the history of what that value 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. Everything that picks out an item counts from zero — the brackets, the item filter, and the numbers beside a list's items in the pane. Pipelines → Lists has what the filters make of one and how it is dealt out, and one item has the brackets in full; here is what one is, and how one is built.

An item is usually a piece of text, but it can be a list or a hash of its own — @<@<a, b>, %home> is a list holding both, and JSON read with json-hash is often a list of hashes. @people[0] hands back whatever the item is, so a hash in a list still has its keys. In the pane such an item is shown rather than typed into, since a field would only edit its text; change it with a command. A list can hold pictures, files and objects too — @<$1, $2> > @shots over two screenshots, or $1 >> @shots one at a time — and the variable keeps each of them for itself, so @shots goes on pasting them after the rows have left the history. An item can be formatted text as well — see Writing formatted text — which pastes formatted, item by item, and reads as its text everywhere else.

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

The plainest way is by hand: List in the + menu of the Variables pane makes an empty one, and its Value column drops down to Add Item. That suits a list you type once and use often — a form's answers, a set of template lines. To put the items in another order, drag the grip (☰) at the start of a row onto the row whose place it should take.

⌘↩ in the overlay gathers rows into a list. It picks rows out the way ⌘⌥T does, starting with the highlighted one: ↩ or ⌘1–⌘9 picks out a row, typing narrows the list, and ⌘A takes everything the search left. ⇥ goes back to the search bar with the list itself written in:

@<$2, $5, $9>

↩ then puts the list on the clipboard, dealt out an item per paste; typing > @name on the end keeps it under a name instead. ⌘↩ again goes back to picking and adds to the end of it.

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.

Hashes

A variable a command has put values under keys in is a hash, addressable as %name. %contact{email} is one value, %contact is all of them, and $contact is the text they make, written out as key: value lines. A key can hold text, a list, another hash, or a picture — $1 > %brand{logo} — and %brand{logo} pastes the picture, or hands it to an image filter: %brand{logo} | resize 50%. One can be written out in the search bar — %<name: $1, email: $2> > %contact — or read out of JSON with json-hash. Pipelines → Hashes has how one is built and how fill turns a template into text from one.

In the pane a hash drops down to a key field and a value field for each key, with Add Key below, and Hash in the + menu makes an empty one to fill in. Changes are kept as you type. A value with no key, or a key used twice, is outlined and not kept until it is put right, so what the pane shows and what %name reads never disagree. A key holding a list, a hash or a picture is shown rather than typed into; change it with a command, such as > %name{key}. Regexes and scripts see the text, since neither deals in anything else.

Pictures, formatting and files

A variable can keep a copy just as it was copied: a picture, text with its formatting, files from Finder, or an app's object such as a Keynote shape. Store any row with ⇧↩ in the overlay, or name it in a command:

$3 > $logo                    the third copy, as it was copied
$1 | resize 50% > $thumb      a smaller picture
$logo                         pastes the picture

Pasting $logo — its row in the overlay, or $logo run from the bar — puts it back the way it was copied. The overlay shows a picture's thumbnail on its row and the picture on ⌘L.

The formatting goes as far as the text it describes, as it does everywhere else. A command that names a copy and does nothing to it keeps it whole. One that runs a filter on it keeps the formatting where most of the text is still there — $1 | upper > $sig keeps the bold, recased — and keeps plain text where the filter wrote something new, such as summarize. >> adds characters, so it keeps text and refuses a picture.

Writing formatted text

The button beside a text variable's value — Aa, or a pencil once it is formatted — opens it in an editor with what TextEdit has: fonts and sizes, text and highlight colours, bold, italic, underline and strikethrough, superscript and subscript, headings and paragraph styles, alignment, spacing, bulleted and numbered lists, tables, links, pictures, find and replace, and spelling. The shortcuts are TextEdit's too: ⌘B, ⌘I, ⌘U, ⌘T for fonts, ⌘K for a link, ⌘F to find.

Save with ⌘S (↩ is a new line). Pasting the variable then gives the formatted text to an app that takes it — a browser gets HTML — and the text alone to one that doesn't. Text nobody formatted stays plain. Its past values are kept, as for any variable.

A formatted value can still be typed over in its row. When you press ↩ or click away, the formatting is kept where the text didn't change, so fixing one digit of a number copied from a web page keeps it as it was. To make a variable plain text, run $name | unfmt > $name in the search bar, or select everything in the editor and clear its formatting.

Each item of a list has the same button in the list's drop-down, so @people[2] can be bold, or carry a link, while the rest stay plain. Save puts it back in its place; dealt out with ↩, each item pastes as it looks. $1 > @people[2] keeps a formatted copy's look the same way, and @people[2] on its own pastes it formatted. A formatted item is shown in the drop-down rather than typed into, so open it with its pencil to change it. Anything that rewrites its text — a filter, or >> adding to it — leaves plain text, as it does for a variable. An export file carries a list's formatted items as their text.

A formatted variable stays formatted when you write it into other text. With a signature in $sig:

"Dear $name,\n\nThanks,\n${sig}"     the signature as you wrote it, the rest plain
$reply | eval                        the same, from a template kept in $reply

Only the variable's own characters keep its look, pictures included; the words around it are plain, or keep the template's own formatting when $reply is formatted too. A copy from the history written into a template — "see $1" — comes out as plain text, as $1 does on its own.

A picture isn't text, so $logo | upper says $logo holds an image; ocr reads the words in it and the image filters work on it, as they do on a row. An abbreviation pastes one, and an export file leaves out a variable holding a picture or an object; a formatted variable goes with its formatting, pictures inside it included. An object stays on the Mac it was copied on, as it does in the history. Pictures sync to your other Macs the way the history's do. Over the iCloud limit, a variable's picture can leave iCloud, after the history's pictures and long copies: the variable keeps syncing, and each Mac keeps the picture it has.

The Variables pane

Shows the variables you have, their values, how many items a list holds and how many values each has held. A variable holding a picture, files or an object shows what it is — with a thumbnail for a picture — rather than a field to type in, and can't be made secret. The sigil in front of the name — $, @, % or $! — is shown rather than typed: what makes an entry a list or a hash is what it holds, chosen when it is made — Variable, List, Hash or Secret in the + menu — or by the command that filled it. 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 fill in a list made from the + menu, or to correct one by hand; the commands — >, >>, split, ⌘↩ in the overlay — are the way to build one from what you copy.

Selecting a variable lists its past values below the table — value, when it was set and what set it, with a thumbnail for a picture — 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.

⌘-click or ⇧-click selects more than one variable, and the minus button then removes them all. A deleted variable waits in the trash for a day: ⌘Z in the overlay puts back the last ones you deleted, and ⌘⇧⌫ shows them all. If you've made another variable with the same name in the meantime, the old one can't come back until that name is free. Past values and Make Secret are for one variable at a time, so they wait until only one is selected.

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. Looking at that history asks for Touch ID or your password first, in the overlay (⌃↩) and in the Variables pane (Show Past Values…), and then shows the values themselves. 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.

While it is open, a secret's row in the overlay shows it when it is selected or the pointer rests on it — under $!, or a sealed copy in the history, including one at the top when the overlay opens — and moving away masks it again. Neither ever asks for Touch ID, so with the vault shut the row just stays ••••••••.

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.
  • A secret reaches a file only when you write it to one. $!token > ~/path writes it while the vault is open, as any other command reads it — and the file is plain text from then on. Save as File, Paste as File and dragging are not offered on a secret's row, which holds nothing until the vault reads it.
  • Deleting vault.key and vault.pub makes every stored secret permanently unreadable.