Skip to content
Eugene Lazutkin edited this page Jul 12, 2026 · 1 revision

jot

Encrypted notes editor using AWS S3 as storage. Files are decoded by extension — for example, notes.md.gz.age is decrypted by age, decompressed by gzip, edited, then re-encoded and uploaded.

Usage

jot                            # List files (default)
jot notes                      # Edit file (prefix match)
jot edit notes.md.gz.age       # Edit file
jot list [prefix]              # List files
jot get notes                  # Download to current directory
jot put local.md remote.md     # Upload file
jot delete notes.md.gz.age     # Delete file
jot rename old.md new.md       # Rename file

If no arguments are provided, jot lists available files. If only one argument is provided, jot edits that file. Commands edit, get, and rename treat their 1st argument as a prefix unless --exact is specified.

Commands

Command Description Arguments
edit, e Edit file file_name
list, l List available files [prefix]
get, g Download to current directory file_name
put, p Upload local file from_name to_name
delete, del, rm, d Delete file file_name
rename, mv, r Rename file from_name to_name

Options

Option Description
-x, --exact Exact file name match (no prefix search)
-y, --yes Create new file without confirmation
-n, --no Don't create new file
-a, --as <chain> Codec chain to apply on put (e.g. gz.age); tar auto-prepended for directories
-f, --for <names> Encrypt to named recipients (comma-separated); see Multi-recipient encryption
-v, --version Show version
-h, --help Show help

Environment variables

Variable Default Description
JOT_AWS_S3_BUCKET (required) AWS S3 bucket name
JOT_AGE_KEY ~/.ssh/age.key Age encryption key
JOT_SSH_PUB auto-detected SSH public key (fallback for encryption) — first of ~/.ssh/{id_ed25519,id_rsa,id_ecdsa}.pub
JOT_SSH_PRV auto-detected SSH private key (fallback for decryption) — first of ~/.ssh/{id_ed25519,id_rsa,id_ecdsa}
JOT_RECIPIENTS_DIR ~/.config/jot/recipients Directory of named recipient public-key files (<name>.pub); used by -f/--for
VISUAL Editor for notes (checked first)
EDITOR nano Editor for notes (fallback)

Supported codecs

Extension Description
.age Age encrypted file
.br Brotli compressed file
.bz2 Bzip2 compressed file
.gz Gzip compressed file
.tar Tar archive (encode tars a directory; decode extracts to a directory)
.xz XZ compressed file
.zst Zstandard compressed file

Naming convention: long-form .tar.gz, not .tgz

jot composes codec chains left-to-right by dot-separated extension (tar.gz.age = tar, then gzip, then age-encrypt). Always write compressed tarballs as .tar.gz / .tar.bz2 / .tar.xz, never as the shortcuts .tgz / .tbz2 / .txz.

The reason is that the shortcuts don't survive composition. .tar.gz.age is unambiguously "tar, then gzip, then age"; .tgz.age would have to be either "tgz (one fused codec) then age" or "tar+gz then age" — jot's parser picks one, but the written name no longer documents the actual pipeline. Long-form is lossless whenever a third codec gets appended, which is the common case (.age and sometimes .zst on top of a tarball).

arx stays lenient on input and accepts both .tar.gz and .tgz when extracting third-party archives — that's a one-off read concern, not a composition concern. In jot output, stay in the long form.

Sharing sensitive files across machines

Sensitive per-machine files (~/.env, ~/.msmtprc, ~/.ssh/age.key, API tokens, SSL certificates) can't live in git or chezmoi. When machines can't reach each other directly (different networks, NAT, cloud instances), jot serves as a secure intermediary — files are encrypted at rest on S3 and in transit over HTTPS.

Naming convention

Use a sys/ prefix to separate shared system files from personal notes:

sys/env.gz.age           # ~/.env
sys/msmtprc.gz.age       # ~/.msmtprc
sys/age-key.age          # ~/.ssh/age.key (tiny, no compression needed)
sys/ssh-config-d.tar.age # ~/.ssh/config.d/* bundled as a tarball

jot list sys/ shows only system files.

Uploading

jot put ~/.msmtprc sys/msmtprc.gz.age

The extension chain drives encoding: .gz.age means gzip then age-encrypt.

Downloading

jot get sys/msmtprc
mv msmtprc ~/.msmtprc
chmod 600 ~/.msmtprc

Jot does not preserve Unix permissions — S3 has no concept of file modes. Set permissions explicitly after download.

File Permissions
~/.env 600
~/.msmtprc 600
~/.ssh/age.key 600
~/.ssh/config.d/* 644 (directory 700)

Updating a shared file

jot edit sys/env           # download, edit in $EDITOR, re-upload
jot put ~/.env sys/env.gz.age -y   # overwrite without confirmation

Directories

Jot supports directories via the .tar codec. When the local path on put is a directory, jot tars it as the first encoding step. There are two ways to drive this:

Spell the chain in the asset name:

jot put ~/.ssh/config.d sys/ssh-config-d.tar.gz.age

Use --as / -a to specify the codec chain:

jot put ~/.ssh/config.d sys/ssh-config-d -a gz.age   # → sys/ssh-config-d.tar.gz.age
jot put ~/.env sys/env -a gz.age                     # → sys/env.gz.age

For directories, tar is auto-prepended to the chain if not already at the front. The chain value is parsed leniently — gz.age, .gz.age, ..gz..age. all produce the same chain.

If you put a directory without --as and the asset name doesn't include .tar, jot warns and appends .tar to the asset name so the data and the name stay in sync.

Download — prefix matching controls how far the codec chain is unwound:

Command Result
jot get sys/ssh-config-d directory ssh-config-d/ (full decode + extract)
jot get sys/ssh-config-d.tar file ssh-config-d.tar (no extract)
jot get sys/ssh-config-d.tar.gz file ssh-config-d.tar.gz (decrypt only)

jot edit refuses to operate on a directory archive — there's no meaningful editor experience for a tarball. Use get / put for directories.

Encryption keys

Jot encrypts with the first key it finds: ~/.ssh/age.key, then the first SSH key present among ~/.ssh/id_ed25519.pub, ~/.ssh/id_rsa.pub, ~/.ssh/id_ecdsa.pub (fallback). Override explicitly with JOT_SSH_PUB / JOT_SSH_PRV. All machines that need to decrypt a file must share the same key. See Setting Up a New Machine — Distributing keys for key distribution.

Multi-recipient encryption

By default jot encrypts to a single key — fine when every machine that needs to decrypt shares the same ~/.ssh/age.key (the recommended setup). When you need to share a file with someone whose key you don't share — a coworker, a temporary client machine, a group with mixed identities — use -f / --for to encrypt to a named set of recipients:

jot put ~/certs/ sys/certs -a gz.age -f bob,jim
jot put ~/.env sys/env -a gz.age -f team

Each name in the comma-separated list maps to a public-key file:

$JOT_RECIPIENTS_DIR/<name>.pub        # default: ~/.config/jot/recipients/

The file contains one or more age public keys (one per line — age-keygen -y extracts a public key from a private key file, or paste a recipient's age1... string directly). A "team" file with multiple keys lets a single name address a group.

Decryption is unchanged: any holder of one of the recipient private keys can jot get and decrypt without flags. The -i argument to age falls through JOT_AGE_KEY then JOT_SSH_PRV as before.

When -f is set, the default age/SSH key path is not used for encryption — only the named recipients are. If you also want to keep your own access, include yourself in the recipients list (-f me,bob,jim with a me.pub containing your own age public key).

The recipients directory is a good fit for chezmoi — public keys aren't secrets, so a chezmoi-managed ~/.config/jot/recipients/ ensures all machines automatically know all named recipients.

Bootstrap sequence

A brand-new machine has no keys and no AWS credentials. The first secrets must arrive out-of-band — via scp, playbash put, USB, or a password manager. Once ~/.env (AWS creds) and the encryption key are in place, jot get unlocks everything else:

out-of-band → ~/.env + age.key → jot get sys/msmtprc → jot get sys/... → done

Bundle helpers

For collecting a defined set of system files (per-host or per-role) and reinstalling them on another machine, two companion tools wrap jot with a manifest-driven workflow.

Manifest

The manifest is a plain text file describing which files to bundle, their target permissions, and (optionally) ownership. Default location: ~/.local/share/jot/sys-files.manifest. An example is shipped at ~/.local/share/jot/sys-files.manifest.example.

# path                 mode   [owner:group]
.env                   600
.msmtprc               600
.ssh/age.key           600
.ssh/config.d/         700
/etc/ssl/local.pem     644    root:root
  • Path without leading / → $HOME-relative (e.g. .env → ~/.env).
  • Path with leading / → absolute, used as-is.
  • Trailing / → directory (recursively bundled and installed).
  • Blank lines and # comments are ignored.
  • owner:group triggers sudo for read (collect) or write (deploy).

The manifest contains no secrets — just paths and metadata — so it's a good candidate for chezmoi management.

jot-bundle — collect and upload

jot-bundle sys/myhost                    # default manifest
jot-bundle sys/myhost -m custom.list     # custom manifest
jot-bundle sys/myhost -f bob,jim         # encrypt to named recipients

Reads the manifest, stages each existing file under a two-tier home/ + root/ layout, embeds the manifest as .jot-manifest, and calls jot put -a tar.gz.age. Files missing on the local machine are warned and skipped; the embedded manifest still lists them, so the deploy side can report them.

jot-deploy — download and install

jot-deploy defaults to report mode (dry-run): it downloads the bundle, walks the embedded manifest, and shows what would change. --apply actually writes.

jot-deploy sys/myhost                    # report only — safe to run anytime
jot-deploy sys/myhost --apply            # install differing files (prompts on overwrite)
jot-deploy sys/myhost --apply -y         # install without overwrite prompts

Per-entry behavior:

Status Meaning
would install / installed File missing on target, present in bundle
would overwrite / overwrote File on target differs from bundle (prompts unless -y)
unchanged Content matches; nothing to do
skipped User answered n to the overwrite prompt
missing in bundle Manifest lists the entry but jot-bundle didn't find it on collect

For files / dirs whose owner column doesn't match the running user, both helpers escalate via sudo — installing /etc/ssl/local.pem will prompt for sudo unless the user has passwordless sudo.

Bootstrap workflow

On the operator machine:

jot-bundle sys/laptop          # gather everything in the manifest, upload as sys/laptop.tar.gz.age

On the new machine (after ~/.env + age key arrived out-of-band):

jot-deploy sys/laptop          # see what's about to change
jot-deploy sys/laptop --apply  # install

The new machine doesn't need its own copy of the manifest — it's embedded in the bundle. bootstrap-dotfiles --from-jot <prefix> wraps this for the SSH-less bootstrap path: prerequisites + brew + chezmoi init --apply + jot-deploy, all in one local script.

Clone this wiki locally