Skip to content

Secrets

A dev note is full of things you cannot commit: a password, a token, an Authorization header. Either the note stays out of git, or the credentials get pushed.

Znote's answer keeps both: the note stays plain markdown and versioned, while its sensitive values live in a neighbouring encrypted file. The markdown holds a reference, never the value:

markdown
**Cisco account**
- Login : jane.doe@example.com
- Password : `secret:cisco.password`

That is the whole trick. Because the note never contains the value, it is safe to commit, to search, to send to the AI and to export. In the editor the reference renders as •••••••• with a reveal and a copy button.

Encryption is done by sops and age — two standard command-line tools, not a Znote format. The encrypted files stay readable from a terminal on any machine that has the key, with or without Znote.

Requirements

Znote uses the sops and age installed on your machine, the same way it uses your git — nothing is bundled, and nothing is a Znote-specific format.

sops 3.10 or later is required. Znote encrypts a new file through sops's standard input, so that the cleartext never touches the disk; older versions cannot read a document that way. Settings tell you the version they found.

Install the two tools

bash
brew install sops age
bash
# age is packaged just about everywhere
sudo apt install age          # or: dnf install age / pacman -S age / apk add age

# sops rarely is — take the binary from its release page
curl -LO https://github.com/getsops/sops/releases/download/v3.13.3/sops-v3.13.3.linux.amd64
sudo install -m 0755 sops-v3.13.3.linux.amd64 /usr/local/bin/sops
sops --version
powershell
# sops is in scoop's main bucket, age in extras
scoop install sops
scoop bucket add extras
scoop install age

sops --version
age-keygen --version

On Linux, swap linux.amd64 for linux.arm64 on ARM, and check the releases page for the current version — the file name carries it, so there is no stable "latest" URL.

On Windows without scoop, take sops-<version>.amd64.exe from the same releases page, rename it sops.exe, and put it on your PATH along with age. winget install FiloSottile.age works for age (that package is the age author's own); for sops the winget entries are community-maintained and one of them is still on 3.7.3, below the version Znote needs — check sops --version before trusting one.

Create your key

bash
mkdir -p ~/.config/sops/age
age-keygen -o ~/.config/sops/age/keys.txt
powershell
New-Item -ItemType Directory -Force "$env:APPDATA\sops\age"
age-keygen -o "$env:APPDATA\sops\age\keys.txt"

age-keygen creates the file with 0600 permissions but does not create its parent folder — hence the mkdir. It prints your public key on the way out: that is the one you give to teammates, and Settings shows it again at any time.

Znote finds it there on its own — leave the age key file field empty and it looks where sops would, on each platform.

Let your terminal find it too

Znote is not the only thing that reads these files: the whole point of a standard format is sops decrypt from a terminal, yours or a teammate's. That path needs one variable:

bash
echo 'export SOPS_AGE_KEY_FILE="$HOME/.config/sops/age/keys.txt"' >> ~/.zshrc
bash
echo 'export SOPS_AGE_KEY_FILE="$HOME/.config/sops/age/keys.txt"' >> ~/.bashrc
powershell
[Environment]::SetEnvironmentVariable("SOPS_AGE_KEY_FILE", "$env:APPDATA\sops\age\keys.txt", "User")

Open a new terminal afterwards. On Linux and Windows the folders above are already sops's own defaults, so the variable is a convenience; on macOS it is not optional — sops reads ~/Library/Application Support/sops/age/keys.txt there unless XDG_CONFIG_HOME says otherwise.

Restart Znote once you are done. An app is handed its environment when it launches, so a variable set — or, on Windows, a binary installed — while Znote was open only reaches it on the next start. The status line in Settings then names the sops version it found.

The ? button next to the age key file field in Settings carries these same recipes, with a copy button on each command.

Znote never stores your key

Settings hold the path to the key file, never its content. A key file located inside the vault is refused — the vault is the git repository, and a private key committed alongside the files it protects protects nothing.

Setup (Settings → Secrets)

FieldWhat it is for
age key fileWhere your private key lives. Left empty, Znote looks where sops itself would: SOPS_AGE_KEY_FILE first, then the standard folder for your platform. The ? button walks through the whole setup, per OS.
sops binaryOnly needed when sops is not on the PATH — on macOS a GUI app does not inherit your login shell.
Block a commit that carries a plaintext secretThe commit guard, on by default.
Private note patternNotes that must never be committed, local-* by default. See Keeping files out of git.

A status line at the top says whether everything is ready — Ready — sops 3.13.3, encrypting for your Znote key — or exactly what is missing. Your public key is shown below the field: that is what you give to a colleague so they can grant you access.

How a note points at a secret

Three forms, two scopes:

FormWritten asWhere
Inline`secret:cisco.password`Anywhere in the text
Interpolation${secret:cisco.password}Inside a code block
Fenced```secret ciscoA whole block of values

The key is a path: cisco.password is the password entry under cisco.

The scope is the prefix, and it decides which file the value lives in:

  • secret:shared. Encrypted and committed, readable by anyone whose key is a recipient.
  • local:personal. Encrypted and never committed — it stays on this machine.

The two are never interchangeable, and there is no fallback from one to the other: a local: reference that resolves nowhere stays unresolved rather than quietly reading the shared file.

Where the values live

Next to the note, one file per scope:

MyVault/
├── Infra.md                      # the note — plain text, committed
├── Infra.secrets.yaml            # shared — encrypted, committed
└── Infra.local.secrets.yaml      # personal — encrypted, never committed

Inside, sops encrypts values, not keys, so the file stays readable as a structure and a one-value change is a three-line git diff:

yaml
cisco:
    password: ENC[AES256_GCM,data:Tr8x...,type:str]
gitlab:
    token: ENC[AES256_GCM,data:9dKq...,type:str]

Never hand-edit a .secrets.yaml

sops authenticates each value together with its key path. Renaming a key in the file by hand makes its value permanently undecryptable. Rename from the secrets panel, which does it as a set + unset.

In the editor

Opening a note that uses secrets decrypts them — there is no padlock to click.

What you seeWhat it means
•••••••• with an eye and a copy buttonResolved. Copy without revealing is right there: putting a password on screen is rarely what you actually wanted.
missing with a +The note references a key the file does not hold. Click + to set it — that also creates the file if this is the note's first secret.
A dashed outlineA local: reference — a value that is never committed.
•••••••• with a padlockSecrets cannot be read here. Click it to hear why (sops missing, key not found, encrypted for another key).

Turning existing text into a secret

Select the password already in your note, right-click → SecretMove selection to the shared secrets… (or local). Znote asks for a key name, encrypts the value, and replaces the selection with the reference. The cleartext never makes a round trip through the file.

Cmd/Ctrl+Shift+S does the same on the current selection, asking which of the two files it goes to.

With no selection, the same menu offers Insert a shared secret…, which drops a reference at the caret for you to fill in later from its +.

The secrets panel

More (…) → Secrets… opens the grid of everything the note has, both scopes side by side. Each row can be revealed, copied, renamed, moved between the shared and the local file, or deleted. Keys the note references but the file does not hold appear as their own row with an input already waiting.

Edits are written immediately — there is no Save button, and nothing is held in memory longer than it has to be.

In code blocks

Inside a code block, a reference is written ${secret:...} and is substituted when the block runs:

markdown
```bash
curl -H "Authorization: Basic ${secret:api.basic_auth}" https://example.com/health
```

How it is substituted depends on what the runner does with your code:

LanguageSubstitution
bash, sh, zsh, cmd, powershell, nodeThe value travels through the environment. The temporary script file holds "$ZNOTE_SECRET_API_BASIC_AUTH", never the password.
Browser jsNothing reaches the disk, so the value is substituted inline as a string literal.
Anything elseReferences are left as they are, and a warning names what could not be interpolated — better than running a command with a missing credential.

Output is masked. If a command prints a secret — an echo, a curl that echoes its header, a login that answers with a token — every known value is replaced with •••••••• before it is displayed and before it is persisted into the note. Without that, the persisted output block would put back into the markdown exactly what the sidecar took out — and a chart snapshotted as an image would carry it into a committed PNG, where no scanner would ever find it.

Copy on a code block substitutes the real values: you asked for them explicitly, and the clipboard is what you are about to paste into a terminal.

Keeping files out of git

Two orthogonal rules, written into the vault's own .gitignore inside a managed block:

# >>> znote secrets (managed) >>>
# Notes and secrets that must never leave this machine.
local-*
*.local.secrets.yaml
# <<< znote secrets <<<
  • local-* — a whole private note, matched by the Private note pattern setting. local-job-hunt.md and its sidecar never leave the machine. Such a note carries a local badge in the tree, because nothing else on screen would tell you it is never committed — and never backed up by git either.
  • *.local.secrets.yaml — a few private values inside an otherwise shared note.

The block is written the first time you push from a vault, and can be written on demand with Write ignore rules and README into this vault in Settings.

The rename trap

.gitignore only governs files git does not already track. Renaming a note that was already committed to local-… keeps it tracked, and keeps pushing it. Znote closes that itself: when it applies the rules it also runs git rm --cached on what they now cover, and tells you. The files stay on your disk — they simply stop being pushed. Versions already pushed stay in the history, so a credential that was really exposed still has to be rotated.

The same button writes .znote/README-secrets.md, a short page explaining to whoever clones the repository how to decrypt without Znote. It is committed on purpose — it is documentation, and it contains no secret. It is written once and never rewritten, so a team can add to it.

The commit guard

Before a commit made from Znote, the notes about to be committed are scanned for plaintext credentials: known token shapes (GitLab, GitHub, AWS, Anthropic, OpenAI, JWT, private keys…), values that follow a credential label in French or English, and high-entropy strings.

If something is found, the push stops and shows what and where:

3 plaintext secrets in 2 notes would be pushed as readable text. Once pushed, rewriting history does not undo it — the secrets would have to be rotated.

Three answers: Cancel (the default — nothing is staged), Commit anyway, and Always allow these, which records the finding in .znote/secrets-allowlist.txt so it never asks again. That file stores SHA-256 hashes, never the string itself, and is committed: silencing a false positive helps the whole team.

A note that already uses references is never flagged — the reference is the fix.

What the guard is, and what it is not

It catches the slip made while working in Znote. Znote installs no git hook, so a commit made from a terminal or another editor does not pass through it. The ignore rules above, on the other hand, apply whatever tool you use.

Through the rest of Znote

ActionWhat happens to the secrets
Rename / move a noteIts sidecars follow. The reference is the note's name, so they have to.
Delete a noteIts sidecars go to the trash with it, and come back on Restore. Deleting permanently deletes them too — an orphan encrypted file is something nobody can ever explain.
PullA sidecar changed on the remote is re-read on the spot: an open note shows the new value without being reopened. On a conflict, as everywhere else, the remote wins and your version goes to the trash.
BackupVault backups include the *.local.secrets.yaml files. They are out of git by design, so a backup is the only copy that exists — they are still encrypted inside the zip.
Search, AI, exportSee the reference, never the value. Nothing to configure: the cleartext is never in the document.

Reading the secrets without Znote

Everything is standard sops, so the values stay reachable from a terminal — which matters the day you are on a colleague's machine, in CI, or simply not in the app:

bash
export SOPS_AGE_KEY_FILE="$HOME/.config/sops/age/keys.txt"

sops decrypt Infra.secrets.yaml                                     # everything
sops decrypt --extract '["cisco"]["password"]' Infra.secrets.yaml   # one value
sops edit Infra.secrets.yaml                                        # change it

.znote/README-secrets.md carries those same commands inside the repository, for whoever clones it.

Sharing with a team

A file is encrypted for a list of recipients (public keys). Znote encrypts for your key, which is enough for one person across several machines and for keeping a repository's notes readable to their author.

Adding a colleague is a sops operation, done once from a terminal, per file:

bash
sops rotate -i --add-age age1<their-public-key> Infra.secrets.yaml

They send you their public key with age-keygen -y <their-key-file> — the README in the repository asks them for exactly that. From then on, both of you decrypt the file with your own key.

What Znote never does

  • It never writes the cleartext into your note — not on run, not on paste, not in a persisted result.
  • It never writes the cleartext to a temporary file — shell and node blocks get an environment variable.
  • It never stores your key, only where to find it, and refuses a key file inside the vault.
  • It never runs sops edit, which waits on an $EDITOR no app window can provide.

Znote — your notes, your files, your machine.