Files
lazysecrets/docs/links.md
T
Karl-Einar Bergstrøm 4377eb98bf feat: share env files as encrypted links (#54)
Share `.env` files as an encrypted `lazysecrets://share/…` link. The files are encrypted with a secret's value, so anyone who can read that secret can open the link.

**Sharing:** `S` → `f` on a repo (all its plain env files), an env file, or a key in one (its file).
- **The key** is `share.key` from the repo's `.lazysecrets.yaml`, else from the config. It can be a `lazysecrets://` link, a Key Vault URL or an `@Microsoft.KeyVault(...)` reference.
- **Without `share.key`,** a picker built on Go to opens, titled "Share .env", with "Pick a secret to encrypt with" in its own border. It lists only secrets, leaving out values from plain `.env` files. Pasting a reference picks it. Picking shares at once, and the picker stays open until the link is copied. If the secret can't be used, the reason shows in red in the picker: it can't be read, or it doesn't meet `share.keyRules` (which rule failed is named). `esc` cancels.
- **`share.keyRules`** (defaults `minLength: 32`, `minClasses: 2`; never under 16) applies when sharing, not when opening.
- **On Azure,** the link pins the key's version, so rotating the key doesn't break old links, and disabling a version revokes them.
- **The command line:** `lazysecrets share [--key <secret>] <env files>`.

**Opening** works by paste, `ctrl+o`, the clipboard offer, the link handler or `lazysecrets <link>`.
1. It first says which repo the link is from, how many files it holds and which key it uses, and decrypts only after `o`.
2. It saves into the repo's clone, found by its remote, showing which files are new, which would replace one there, and which git doesn't ignore.
3. Without a clone, it asks for a folder and saves straight away. The prompt stays open, with the reason in red, until the files are written.
4. If decryption fails, a popup says why: the first line of the reason, `r` for the whole message, `g` to go to the key's secret. A key that has changed is named by where it lives: a Databricks or SOPS value changed in place, an Azure current version that isn't the one the link was made with, or a pinned version, meaning the link may be damaged.

**Format** (`internal/share`): a version byte, then a JSON header with the key, the repo and the file count, then an age file (scrypt 2^15, with the key's value as passphrase) holding the files as deflated JSON, all as unpadded base64url. Links are capped at 16 KB. A future format gets a new version, and old ones stay readable.

**Safety:**
- A link can only write env files, inside the folder: never in `.git`, never through a symlink.
- New files are created with mode 0600.
- Opening accepts an scrypt work factor of at most 2^20, and unpacks at most 1 MB.

**Along the way:**
- Opening a link to a secret ends on its details, as `enter` would.
- `o` on an Azure secret opens its portal page, and in the versions view, that version's page.
- The details pane follows its secret's list when it reloads in the background. Before, it kept what it showed first, including an old portal address.
- `ref.Read` is shared by `get` and sharing, with Azure `VersionedValue` for pinning.
- `provider.Contents` lets the demo repos share their env files. `--demo` has a share key (`kv-shared-prod / env-share`), so the whole flow works there.
- New dependency: `filippo.io/age`.

Tested end to end in the TUI and the demo, the CLI, and the `share` package. In a real vault: version pinning, opening into a clone, and revocation by disabling the version.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Reviewed-on: #54
2026-10-13 14:55:22 +02:00

8.3 KiB

Links

Subscriptions, workspaces, vaults, scopes, repos, files and secrets can be shared as a lazysecrets:// link, in chat or a ticket. Whoever opens it lands on that item, reading it with their own account and permissions: the link names the item, never its value. Env files can also be shared encrypted, in the link itself.

lazysecrets://azure/kv-app-prod.vault.azure.net                              vault
lazysecrets://azure/kv-app-prod.vault.azure.net/keys                         vault, Keys tab
lazysecrets://azure/kv-app-prod.vault.azure.net/secrets/db-password          secret
lazysecrets://azure/kv-app-prod.vault.azure.net/secrets/db-password/91d3e0   one version
lazysecrets://databricks/adb-1111111111111111.11.azuredatabricks.net/app/storage-key
lazysecrets://repo/git.example.com%2Fteam%2Finfra/secrets%2Fapp.yaml/db.password

Sharing

s copies a link to the selected item (from the versions view, pinned to that version). S opens the share menu:

  • secrets: the link (o), the plain Key Vault URL (u), the config reference (y) or the web page (b); in an env file, also the file as an encrypted link (f)
  • vaults and scopes: a link (o), a link that opens on the current tab (t), the plain vault URL (u) or the web page (b)
  • subscriptions and workspaces: a link (o) or the web page (b)
  • repos and env files: also their env files as an encrypted link (f)

Sharing env files

.env files usually live only on your machine. S → f shares them as an encrypted link, on a repo (all its plain env files), an env file, or a key in one (its file):

lazysecrets://share/AQ…

The files are encrypted with a secret's value, so whoever can read that secret (in Key Vault, Databricks or a repo's SOPS file) can open the link, and nobody else: who may open it is up to the vault's access, as for everything else. Make a secret for it once, random:

az keyvault secret set --vault-name kv-team --name env-share --value "$(openssl rand -base64 32)"

and name it in the repo's .lazysecrets.yaml (committed: the whole team uses it) or your config (for repos that don't name one), by its lazysecrets:// link, Key Vault URL or @Microsoft.KeyVault(...) reference:

share:
  key: https://kv-team.vault.azure.net/secrets/env-share

With neither, sharing asks which secret, in Go to: it lists only secrets, and enter picks one (pasting a reference picks it too).

Anyone with a link can try to guess the key, so sharing refuses one that isn't long and varied enough, saying how (never showing it). By default that's 32 characters of two kinds; your config can ask for more, or less:

share:
  keyRules:
    minLength: 32    # characters; 16 at least
    minClasses: 2    # kinds of character: lowercase, uppercase, digits, symbols

Opening a link doesn't check them.

On the command line, lazysecrets share [--key <secret>] <env file>... prints the link.

Opening a share link says which repo it's from, how many files and which secret it's encrypted with, and decrypts only when asked: lazysecrets reads the secret with your access, then shows where each file goes in your clone of the repo (or asks for a folder, if it isn't here), which ones would replace a file there and which ones git doesn't ignore. It saves them all, only the new ones, or none. New files are readable by you only.

A link holds the files' paths and contents encrypted (age, the secret's value as passphrase); the repo, the secret and the number of files are in the clear. Links stay under 16 KB, which fits chat and tickets.

A link stays readable as long as its secret is. On Azure it's pinned to the secret's version, so adding a new version (rotating) doesn't break old links, and disabling the old version, or taking away someone's access, shuts them out; Databricks secrets have no versions, so changing the value breaks every old link. Whoever has read the secret's value before can decrypt links made with it without asking the vault, so rotate it now and then: new links use the newest version.

Opening

  • Paste it into lazysecrets, or press ctrl+o, which reads the clipboard and fills in any reference it finds there. Key Vault URLs, @Microsoft.KeyVault(...) and {{secrets/scope/key}} work too.
  • lazysecrets <link> (or lazysecrets open <link>) in a terminal.
  • Click it anywhere, once the link handler is set up.

If you copy a lazysecrets:// link elsewhere and switch back, lazysecrets offers to open it, once per link. It looks at the clipboard at startup, when the terminal regains focus (in tmux: set -g focus-events on) and, for terminals that don't report focus, on a key press at most every 3 seconds. It keeps nothing but lazysecrets:// links, doesn't offer a link to what's already selected (like one you just copied with s), and can't read the clipboard over SSH.

lazysecrets finds the vault in your accounts (one Resource Graph query per account) and jumps to it, asking which account to use when several can see it, ending on a secret's details as enter on it would. Links say what, not what to do: opening a link to a secret asks whether to just go to it, also copy or show its value, or (y) copy its value without going there.

In scripts

lazysecrets get <link> prints the value, for scripts and pipes:

export DB_PASSWORD=$(lazysecrets get lazysecrets://azure/kv-app-prod.vault.azure.net/secrets/db-password)
lazysecrets get lazysecrets://azure/kv-app-prod.vault.azure.net/secrets/db-password | kubectl create secret generic db --from-file=password=/dev/stdin

It reads the value directly, with your az accounts (the one that last read it first), the Databricks profile for the workspace, or sops in your clone of the repo; a version in the link reads that version. The value ends with a newline only on a terminal.

From the browser and chat

lazysecrets handler install     # run it in the terminal links should open in
lazysecrets handler uninstall

Use an installed binary (go run's is temporary). lazysecrets offers to do this at startup when links don't open it (or open a lazysecrets that no longer exists): set it up, not now, or don't ask again, which is remembered in the state file. linkHandler.prompt: false in the config turns the question off too.

handler install registers lazysecrets:// with the system: clicking a link then opens a new window of the terminal you ran it in, with lazysecrets on that link. It recognises kitty, Ghostty, WezTerm, iTerm2, Terminal.app, Alacritty, GNOME Terminal, Konsole and foot, also inside tmux; --terminal NAME picks another. It records your PATH and config locations, since apps start without your shell's environment (and so without az): run it again after moving lazysecrets or changing those.

  • macOS: a small background app, ~/Applications/Lazysecrets Links.app
  • Linux: a desktop entry claiming x-scheme-handler/lazysecrets, set as the default with xdg-mime (desktops following freedesktop.org)
  • Windows: not yet

The config can override the terminal, or run any other:

linkHandler:
  terminal: ghostty                          # instead of the one install detected
  command: [foot, --title, secrets, "{cmd}"] # any terminal; {cmd} is the lazysecrets command
  reuse: true                                # hand links to a running lazysecrets first
  prompt: true                               # offer to set up the handler at startup
  • In a terminal (a link in a terminal app): a lazysecrets that terminal shows (or whose tmux session one of its tmux clients can switch to) gets it, never one elsewhere; otherwise a new window opens.
  • Anywhere else (Slack, a browser): the lazysecrets you used last (key press or focus), preferring one already on screen; a tmux client is switched to its session if needed, its terminal comes to the front, and a hidden dropdown terminal (kitty's quick access terminal) is shown. With none running, a new window opens.

The link is then offered as if pasted. linkHandler.reuse: false always opens a new window.

Running instances listen on a socket in $XDG_RUNTIME_DIR/lazysecrets, or run/ in the state directory, readable only by you. A link from a web page can only make lazysecrets go somewhere: it asks before copying or showing anything.