Repository navigation
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.
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.
| 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 |
| 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 |
| 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) |
| 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 |
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.
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.
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.
jot put ~/.msmtprc sys/msmtprc.gz.ageThe extension chain drives encoding: .gz.age means gzip then age-encrypt.
jot get sys/msmtprc
mv msmtprc ~/.msmtprc
chmod 600 ~/.msmtprcJot 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) |
jot edit sys/env # download, edit in $EDITOR, re-upload
jot put ~/.env sys/env.gz.age -y # overwrite without confirmationJot 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.ageUse --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.ageFor 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.
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.
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 teamEach 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.
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
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.
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:grouptriggerssudofor 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 sys/myhost # default manifest
jot-bundle sys/myhost -m custom.list # custom manifest
jot-bundle sys/myhost -f bob,jim # encrypt to named recipientsReads 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 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 promptsPer-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.
On the operator machine:
jot-bundle sys/laptop # gather everything in the manifest, upload as sys/laptop.tar.gz.ageOn 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 # installThe 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.
Initial setup
General information
- Shell Environment
- Git Configuration
- Application Notes
- Remote GUI
- Utilities
- Playbash Server Management
Day-to-day operations