diff --git a/README.md b/README.md
index 3344e63..65eef93 100644
--- a/README.md
+++ b/README.md
@@ -19,56 +19,135 @@
`pda!` is a command-line key-value store tool with:
-- [templates](https://github.com/Llywelwyn/pda#templates) supporting arbitrary shell execution, conditionals, loops, more,
-- [encryption](https://github.com/Llywelwyn/pda#encryption) at rest using [age](https://github.com/FiloSottile/age),
-- Git-backed [version control](https://github.com/Llywelwyn/pda#git) with automatic syncing,
-- [search and filtering](https://github.com/Llywelwyn/pda#filtering) by key, value, or store,
+- [templates](#templates) supporting arbitrary shell execution, conditionals, loops, more,
+- [encryption](#encryption) at rest using [age](https://github.com/FiloSottile/age),
+- Git-backed [version control](#git) with automatic syncing,
+- [search and filtering](#filtering) by key, value, or store,
- plaintext exports in 7 different formats,
-- support for all [binary data](https://github.com/Llywelwyn/pda#binary),
-- expiring keys with a [time-to-live](https://github.com/Llywelwyn/pda#ttl),
-- [read-only](https://github.com/Llywelwyn/pda#read-only) keys and [pinned](https://github.com/Llywelwyn/pda#pinned) entries,
-- built-in [diagnostics](https://github.com/Llywelwyn/pda#doctor) and [configuration](https://github.com/Llywelwyn/pda#config),
+- support for all [binary data](#binary-data),
+- expiring keys with a [time-to-live](#ttl),
+- [read-only](#read-only) keys and [pinned](#pinned) entries,
+- built-in [diagnostics](#doctor) and [configuration](#config),
and more, written in pure Go, and inspired by [skate](https://github.com/charmbracelet/skate) and [nb](https://github.com/xwmx/nb).
-`pda!` stores key-value pairs natively as [newline-delimited JSON](https://en.wikipedia.org/wiki/JSON_streaming#Newline-delimited_JSON) files. The `list` command outputs tabular data by default, but also supports [CSV](https://en.wikipedia.org/wiki/Comma-separated_values), [TSV](https://en.wikipedia.org/wiki/Tab-separated_values), [Markdown](https://en.wikipedia.org/wiki/Markdown) and [HTML](https://en.wikipedia.org/wiki/HTML_element#Tables) tables, JSON, and raw NDJSON. Because every store is in plaintext, Git versioning is pretty easy: auto-committing, pushing, and fetching can be enabled in the config to automatically version changes, or just `pda sync` regularly.
+`pda!` stores key-value pairs natively as [newline-delimited JSON](https://en.wikipedia.org/wiki/JSON_streaming#Newline-delimited_JSON) files. Every store is plaintext, portable, and yours. There's no daemon, no cloud service, and no proprietary format. Keys are just lines in a JSON file; stores are just files in a directory. If you can `cat` a file, you can read your data without `pda!` installed.
+
+Git versioning is built in. Enable auto-committing, pushing, and fetching in the [config](#config) to automatically version every change, or just run [`pda sync`](#sync) when you want to. Because the storage format is line-oriented plaintext, diffs are meaningful and merges are clean.
+
+Go's [`text/template`](https://pkg.go.dev/text/template) engine is available on every value at retrieval time, turning simple key-value pairs into dynamic snippets with variables, environment lookups, shell execution, cross-references, and more.
-### Contents
+### Installation
-- [Overview](https://github.com/Llywelwyn/pda#overview)
-- [Installation](https://github.com/Llywelwyn/pda#installation)
-- [Get Started](https://github.com/Llywelwyn/pda#get-started)
-- [Git-backed version control](https://github.com/Llywelwyn/pda#git)
-- [Templates](https://github.com/Llywelwyn/pda#templates)
-- [Filtering](https://github.com/Llywelwyn/pda#filtering)
-- [TTL](https://github.com/Llywelwyn/pda#ttl)
-- [Read-only](https://github.com/Llywelwyn/pda#read-only)
-- [Pinned](https://github.com/Llywelwyn/pda#pinned)
-- [Binary](https://github.com/Llywelwyn/pda#binary)
-- [Encryption](https://github.com/Llywelwyn/pda#encryption)
-- [Doctor](https://github.com/Llywelwyn/pda#doctor)
-- [Config](https://github.com/Llywelwyn/pda#config)
-- [Environment](https://github.com/Llywelwyn/pda#environment)
+
+
+ ↑
+ Prerequisites ·
+ Build ·
+ Shell Completion
+
+
+
+#### Prerequisites
+
+`pda` has no mandatory requirements outside of a shell to run it in. However, it is enhanced by other tools being installed.
+
+- [go](https://go.dev) is needed for compiling the `pda` binary.
+- [git](https://git-scm.com) enhances `pda` with [version control](#git).
+
+#### Build
+
+The easiest (and most universal) way to install `pda` is to use `go install` to build from source. The same command can be used to update.
+
+```bash
+go install github.com/llywelwyn/pda@latest
+
+# Or from a spceific commit.
+git clone https://github.com/llywelwyn/pda
+cd pda
+go install
+```
+
+[Arch Linux](https://archlinux.org) users can install and update `pda` from the [aur](https://aur.archlinux.org) with a package manager of choice. There are two packages available: `pda`, the latest stable release, and `pda-git`, which will install the latest commit to the main branch on this repository.
+
+```bash
+# Latest stable release
+yay -S pda
+
+# Latest commit
+yay -S pda-git
+
+# Updating
+yay -Syu pda
+```
+
+#### Setting up Shell Completion
+
+`pda` is built with [cobra](https://cobra.dev) and so comes with shell completions for bash, zsh, fish, and powershell.
+
+```bash
+# Bash
+pda completion bash > /etc/bash_completion.d/pda
+
+# Zsh
+pda completion zsh > "${fpath[1]}/_pda"
+
+# Fish
+pda completion fish > ~/.config/fish/completions/pda.fish
+
+# Powershell
+pda completion powershell | Out-String | Invoke-Expression
+```
+
+Powershell users will need to manually add the above command to their profile; the given command will only instantiate `pda` for the current shell instance.
-### Overview
+## Overview
- ```bash
+
+
+
+
+```bash
pda! MIT licensed. (c) 2025 Lewis Wynne
Usage:
@@ -110,747 +189,593 @@ Additional Commands:
-Most commands have aliases and flags. `pda help [command]` to see them.
+Most commands have aliases and flags. Run `pda help [command]` to see them.
-
+### Key commands
-### Installation
+
+
+ ↑ ·
+ pda set,
+ pda get,
+ pda run,
+ pda list,
+ pda edit,
+ pda move,
+ pda remove
+
+
+
+Use of `pda` revolves around creating keys with [`pda set`](#setting) and later retrieving them with [`pda get`](#getting). Keys can belong to a single store which can be set manually or left to default to the default store. Keys can be modified with [`pda edit`](#editing) and [`pda meta`](#metadata) for content or metadata editing respectively, and can be listed with [`pda list`](#listing). Keys are written as `KEY[@STORE]`. The default store can be configured with `store.default_store_name`.
+
+Keys are capable of storing any arbitrary bytes and are not limited to just text.
+
+Advanced usage of `pda` revolves around [templates](#templates) and [`pda run`](#running).
+
+#### Setting
+
+[`pda set`](#setting) (alias: [`s`](#setting)) creates a key-value pair. Values can come from arguments, stdin, or a file.
```bash
-# Get the latest release from the AUR
-yay -S pda
+Usage:
+ pda set KEY[@STORE] [VALUE] [flags]
-# Or use pda-git for the latest commit
-yay -S pda-git
+Aliases:
+ set, s
-# Go install
-go install github.com/llywelwyn/pda@latest
-
-# Or
-git clone https://github.com/llywelwyn/pda
-cd pda
-go install
+Flags:
+ -e, --encrypt encrypt the value at rest using age
+ -f, --file string read value from a file
+ --force bypass read-only protection
+ -h, --help help for set
+ -i, --interactive prompt before overwriting an existing key
+ --pin pin the key (sorts to top in list)
+ --readonly mark the key as read-only
+ --safe do not overwrite if the key already exists
+ -t, --ttl duration expire the key after the provided duration (e.g. 24h, 30m)
```
-
+[`pda set`](#setting) requires a key and a value as inputs. The first argument given will always be used to determine the key.
-### Get Started
-
-`pda set` to save a key.
```bash
-# From arguments
+# create a key-value pair
pda set name "Alice"
-# From stdin
-echo "Alice" | pda set name
-cat dogs.txt | pda set dogs
-pda set kitty < cat.png
+# create a key-value pair with piped input
+echo "Bob" | pda set name
-# From a file
-pda set dogs --file dogs.txt
-pda set kitty -f cat.png
+# create a key-value pair with redirection
+pda set example < silmarillion.txt
-# --safe to skip if the key already exists.
-pda set name "Alice" --safe
+# create a pinned key-value pair from a file
+pda set --pin example --file example.md
+
+# create a key-value pair in the "Favourites" store
+pda set movie@favourites "The Road"
+
+# create an encrypted key-value pair, expiring in one day
+pda set secret "Secret data." --encrypt --ttl 24h
+```
+
+The `interactive` and `safe` flags exist to prevent accidentally overwriting an existing key when creating a new one. These flags exist on all writable commands.
+
+```bash
+# prevent ever overwriting an existing key
pda set name "Bob" --safe
-pda get name
-# Alice
-# --readonly to protect a key from modification.
-pda set api-url "https://prod.example.com" --readonly
+# guarantee a prompt when overwriting an existing key
+pda set name "Joe" --interactive
```
-
+Making a key `readonly` will also prevent unintended changes. It prevents making any changes unless `force` is passed or the key is made writable once again with [`pda edit`](#editing) or [`pda meta`](#metadata).
-`pda get` to retrieve it.
```bash
-pda get name
-# Alice
+# create a readonly key-value pair
+pda set repo "https://github.com/llywelwyn/pda" --readonly
-# Or run it directly.
-pda run name
-# same as: pda get name --run
-
-# Check if a key exists (exit 0 if found, exit 1 if not).
-pda get name --exists
+# force-overwrite a readonly key-value pair
+pda set dog "A four-legged mammal that isn't a cat." --force
```
-
+#### Getting
+
+[`pda get`](#getting) (alias: [`g`](#getting)) retrieves a key's value. [Templates](#templates) are evaluated at retrieval time.
-`pda edit` to open a key in your `$EDITOR`.
```bash
-# Edit an existing key.
+Usage:
+ pda get KEY[@STORE] [flags]
+
+Aliases:
+ get, g
+
+Flags:
+ -b, --base64 view binary data as base64
+ --exists exit 0 if the key exists, exit 1 if not (no output)
+ -h, --help help for get
+ --no-template directly output template syntax
+ -c, --run execute the result as a shell command
+```
+
+[`pda get`] takes one argument: the desired key. The value is output to stdout.
+
+```bash
+# get the value of a key
+❯ pda get name
+Alice
+```
+
+As mentioned in [setting](#setting), values support any arbitrary bytes. Values which are not valid UTF8 are retrieved slightly differently. Printing raw bytes directly in the terminal can (and will) cause [undefined behaviour](https://en.wikipedia.org/wiki/Undefined_behavior), so if a TTY is detected then a raw `pda get` will return instead some metadata about the contents of the bytes. In a non-TTY setting (when the data is piped or redirected), the raw bytes will be returned as expected.
+
+If a representation of the bytes in a TTY is desired, the `base64` flag provides a safe way to view them.
+
+```bash
+# get the information of a non-UTF8 key
+❯ pda get cat_gif
+(size: 101.2k, image/gif)
+
+# get the raw bytes of a non-UTF8 key via pipe
+pda get cat_gif | xdg-open
+
+# get the raw bytes of a non-UTF8 key via redirect
+pda get cat_gif > cat.gif
+
+# get the base64 representation of a non-UTF8 key
+❯ pda get cat_gif --base64
+R0lGODlhXANYAvf/MQAAAAEBAQICAgMDAwQEBAUFBQYGBggI...
+```
+
+The existence of a key can be checked with `exists`. It returns a `0 exit code` on an existent key, or a `1 exit code` on a non-existent one. This is primarily useful for scripting.
+
+```bash
+# check if an existent key exists
+❯ pda get name --exists
+exit code 0
+
+# check if a non-existent key exists
+❯ pda get nlammfd --exists
+exit code 1
+```
+
+Running [`pda get`](#getting) will resolve templates in the stored key at run-time. This can be prevented with the `no-template` flag.
+
+```bash
+# set key "user" to a template of the USER environment variable
+❯ pda set user "{{ env "USER" }}"
+
+# get a templated key
+❯ pda get user
+lew
+
+# get a templated key without resolving the template
+❯ pda get user
+{{ env "USER" }}
+```
+
+An alternative to [templates](#templates) is the `run` flag. For detailed information, see [`pda run`](#running), an alias for `pda get --run`.
+
+```bash
+# create a key containg a script
+❯ pda set my_script "echo Hello, world."
+
+# get and run a key using $SHELL
+❯ pda get my_script --run
+Hello, world.
+```
+
+#### Running
+
+[`pda run`](#running) retrieves a key and executes it as a shell command. It uses the shell set in $SHELL. If, somehow, this environment variable is unset, it falls back and attempts to use `/bin/sh`. Templates are functional when running a key directly.
+
+```bash
+Usage:
+ pda run KEY[@STORE] [flags]
+
+Flags:
+ -b, --base64 view binary data as base64
+ -h, --help help for run
+ --no-template directly output template syntax
+```
+
+Running takes one argument: the key.
+
+```bash
+# create a key containing a script, and a template
+❯ pda set greet 'echo "Hello, {{ default "Jane Doe" .NAME }}"'
+
+# run the key directly in $SHELL
+❯ pda run greet
+Hello, Jane Doe
+
+# run the key, setting NAME to "Alice"
+❯ pda run greet NAME="Alice"
+Hello, Alice
+```
+
+#### Listing
+
+[`pda list`](#listing) (alias: [`ls`](#listing)) shows what you've got stored. The default columns are `meta,size,ttl,store,key,value`. Meta is a 4-char flag string: `(e)ncrypted (w)ritable (t)tl (p)inned`, or a dash for an unset flag.
+
+```bash
+❯ pda ls
+Meta Size TTL Store Key Value
+-w-p 5 - store todo don't forget this
+---- 23 - store url https://prod.example.com
+-w-- 5 - store name Alice
+```
+
+By default, [`pda list`](#listing) shows entries from every store. Pass a store name to narrow to a single store:
+
+```bash
+pda ls @store
+```
+
+Use [`--store`](#filtering) / `-s` to filter stores by [glob pattern](#filtering):
+
+```bash
+pda ls --store "prod*"
+```
+
+Filter by key or value with [`--key`](#filtering) / `-k` and [`--value`](#filtering) / `-v`:
+
+```bash
+pda ls --key "db*" --value "**localhost**"
+```
+
+Columns can be toggled with `--no-X` flags. `--no-X` suppresses a column; `--no-X=false` adds it even if it's not in the default config:
+
+```bash
+# hide the meta and size columns
+pda ls --no-meta --no-size
+```
+
+Long values are truncated to fit the terminal. [`--full`](#listing) / `-f` shows the complete value:
+
+```bash
+❯ pda ls
+Key Value
+note this is a very long (..30 more chars)
+
+❯ pda ls --full
+Key Value
+note this is a very long value that keeps on going and going
+```
+
+[`--count`](#listing) / `-c` prints only the count of matching entries:
+
+```bash
+❯ pda ls --count
+3
+
+❯ pda ls --count --key "d*"
+1
+```
+
+[`--format`](#listing) / `-o` selects the output format. Available formats: `table` (default), `csv`, `tsv`, `json`, `ndjson`, `markdown`, `html`:
+
+```bash
+❯ pda ls --format csv
+Meta,Size,TTL,Store,Key,Value
+-w--,5,-,store,name,Alice
+
+❯ pda ls --format json
+[{"key":"name","value":"Alice","encoding":"text","store":"store"}]
+```
+
+[`--all`](#listing) / `-a` lists across all stores (default when `list.always_show_all_stores` is true).
+
+[`--base64`](#listing) / `-b` shows binary data as base64.
+
+[`--no-header`](#listing) suppresses the header row.
+
+[Pinned](#pinned) entries sort to the top, preserving alphabetical order within the pinned and unpinned groups.
+
+
+
+ See also:
+ pda help list
+
+
+
+#### Editing
+
+
+
+ ↑ ·
+ pda edit,
+ pda set,
+ pda meta
+
+
+
+[`pda edit`](#editing) (alias: [`e`](#editing)) opens a key's value in your `$EDITOR`. If the key doesn't exist, an empty file is opened — saving non-empty content creates it.
+
+```bash
+# edit an existing key
pda edit name
-# Edit a key that doesn't exist yet — saving non-empty content creates it.
+# edit a new key — saving non-empty content creates it
pda edit newkey
+```
-# Edit and modify metadata in the same operation.
+Metadata flags can be passed alongside the edit to modify metadata in the same operation:
+
+```bash
pda edit name --ttl 1h --encrypt
+```
-# Trailing newlines added by the editor are stripped by default.
-# Pass --preserve-newline to keep them.
+Trailing newlines added by the editor are stripped by default. [`--preserve-newline`](#editing) keeps them:
+
+```bash
pda edit name --preserve-newline
```
-
+[`--encrypt`](#editing) / `-e` encrypts the value. [`--decrypt`](#editing) / `-d` decrypts it. [`--readonly`](#editing) and [`--writable`](#editing) toggle protection. [`--pin`](#editing) and [`--unpin`](#editing) toggle pinning. [`--ttl`](#editing) sets or clears expiry (e.g. `30m`, `2h`, or `never`).
+
+Binary values are presented as base64 for editing and decoded back on save.
+
+[Read-only](#read-only) keys require [`--force`](#editing) to edit.
+
+
+
+ See also:
+ pda help edit
+
+
+
+#### Moving & Copying
+
+
+
+ ↑ ·
+ pda move,
+ pda copy
+
+
+
+[`pda move`](#moving--copying) (alias: [`mv`](#moving--copying)) moves a key to a new name or store. All metadata is preserved.
-`pda meta` to view or modify metadata for a key.
```bash
-pda meta session
-# key: session@store
-# secret: false
-# writable: true
-# pinned: false
-# expires: 59m30s
+❯ pda mv name name2
+ ok renamed name to name2
```
-Metadata flags like `--ttl`, `--encrypt`, `--readonly`, and `--pin` are covered in their dedicated sections below.
-
+[`pda copy`](#moving--copying) (alias: [`cp`](#moving--copying)) makes a copy. The source is kept and all metadata is preserved.
-`pda mv` to move it.
```bash
-pda mv name name2
-# ok renamed name to name2
+pda cp name name2
+```
-# --safe to skip if the destination already exists.
+[`mv --copy`](#moving--copying) and [`cp`](#moving--copying) are equivalent:
+
+```bash
+pda mv name name2 --copy
+```
+
+Move or copy across stores:
+
+```bash
+pda mv name@store name@archive
+pda cp config@dev config@prod
+```
+
+[`--safe`](#moving--copying) skips if the destination already exists:
+
+```bash
pda mv name name2 --safe
# info skipped 'name2': already exists
+```
-# --yes/-y to skip confirmation prompts.
+[`--yes`](#moving--copying) / `-y` skips all confirmation prompts:
+
+```bash
pda mv name name2 -y
```
-`pda cp` to make a copy. All metadata is preserved.
+[Read-only](#read-only) keys can't be moved or overwritten without [`--force`](#moving--copying):
+
```bash
-pda cp name name2
+❯ pda mv readonly-key newname
+FAIL cannot move 'readonly-key': key is read-only
-# 'mv --copy' and 'cp' are aliases. Either one works.
-pda mv name name2 --copy
-
-# Read-only keys can't be moved or overwritten without --force.
-pda mv readonly-key newname
-# FAIL cannot move 'readonly-key': key is read-only
pda mv readonly-key newname --force
```
-
+[`cp`](#moving--copying) can copy a read-only key freely (since the source isn't modified), and the copy preserves the read-only flag. Overwriting a read-only destination is blocked without [`--force`](#moving--copying).
+
+
+
+ See also:
+ pda help move,
+ pda help copy
+
+
+
+#### Removing
+
+
+
+ ↑ ·
+ pda remove,
+ --key
+
+
+
+[`pda remove`](#removing) (alias: [`rm`](#removing)) deletes one or more keys.
-`pda rm` to delete one or more keys.
```bash
pda rm kitty
+```
-# Remove multiple keys.
+Remove multiple keys at once:
+
+```bash
pda rm kitty dog@animals
+```
-# Mix exact keys with glob patterns.
+Mix exact keys with [glob patterns](#filtering) using [`--key`](#removing):
+
+```bash
pda set cog "cogs"
pda set dog "doggy"
pda set kitty "cat"
pda rm kitty --key "?og"
+```
-# Opt in to a confirmation prompt with --interactive/-i (or always_prompt_delete in config).
+Filter by store with [`--store`](#removing) / `-s` and by value with [`--value`](#removing) / `-v`:
+
+```bash
+pda rm --store "temp*" --key "session*"
+```
+
+[`--interactive`](#removing) / `-i` prompts before each deletion (or set `key.always_prompt_delete` in [config](#config)):
+
+```bash
pda rm kitty -i
# ??? remove 'kitty'? (y/n)
# ==> y
+```
-# --yes/-y to auto-accept all confirmation prompts.
+Glob-matched deletions prompt by default (configurable with `key.always_prompt_glob_delete`).
+
+[`--yes`](#removing) / `-y` auto-accepts all confirmation prompts:
+
+```bash
pda rm kitty -y
+```
+
+[Read-only](#read-only) keys can't be deleted without [`--force`](#removing):
+
+```bash
+❯ pda rm protected-key
+FAIL cannot remove 'protected-key': key is read-only
-# Read-only keys can't be deleted without --force.
-pda rm protected-key
-# FAIL cannot remove 'protected-key': key is read-only
pda rm protected-key --force
```
-
+
+
+ See also:
+ pda help remove
+
+
-`pda ls` to see what you've got stored. The default columns are `meta,size,ttl,store,key,value`. Meta is a 4-char flag string showing `(e)ncrypted (w)ritable (t)tl (p)inned`, or a dash for an unset flag. Pinned entries sort to the top.
+### Metadata
+
+
+
+ ↑ ·
+ pda meta,
+ TTL,
+ Encryption,
+ Read-Only,
+ Pinned
+
+
+
+[`pda meta`](#metadata) views or modifies metadata for a key without changing its value. With no flags, it displays the key's current metadata:
-By default it lists the contents of all stores. Pass a store name to check only the given store. Checking a specific store is faster than checking everything, but the slowdown should be insignificant unless you have masses of different stores. `list.always_show_all_stores` can be set to false to list only the default store when none is specified.
```bash
-pda ls
-# Meta Size TTL Store Key Value
-# -w-p 5 - store todo don't forget this
-# ---- 23 - store url https://prod.example.com
-# -w-- 5 - store name Alice
-
-# Narrow to a single store.
-pda ls @store
-
-# Or filter stores by glob pattern.
-pda ls --store "prod*"
-
-# Suppress or add columns with --no-X flags.
-# --no-X suppresses. --no-X=false adds even if not in default config.
-
-# Or as CSV.
-pda ls --format csv
-# Meta,Size,TTL,Store,Key,Value
-# -w--,5,-,store,name,Alice
-
-# Or as a JSON array.
-pda ls --format json
-# [{"key":"name","value":"Alice","encoding":"text","store":"store"}]
-
-# Or TSV, Markdown, HTML, NDJSON.
-
-# Just the count of entries.
-pda ls --count
-# 2
-pda ls --count --key "d*"
-# 1
+❯ pda meta session
+ key: session@store
+ secret: false
+ writable: true
+ pinned: false
+ expires: 59m30s
```
-
+Pass flags to modify: [`--ttl`](#ttl), [`--encrypt`](#encryption) / [`--decrypt`](#encryption), [`--readonly`](#read-only) / [`--writable`](#read-only), [`--pin`](#pinned) / [`--unpin`](#pinned).
+
+Multiple metadata changes can be combined in one call:
-Long values are truncated to fit the terminal. Use `--full`/`-f` to show the complete value.
```bash
-pda ls
-# Key Value
-# note this is a very long (..30 more chars)
-
-pda ls --full
-# Key Value
-# note this is a very long value that keeps on going and going
+pda meta session --ttl 2h --encrypt --pin
```
-
+Modifying a [read-only](#read-only) key's metadata requires [`--force`](#metadata) (except for toggling the read-only flag itself, and pin/unpin):
-`pda export` to export everything as NDJSON.
```bash
-pda export > my_backup
+❯ pda meta api-url --ttl 1h
+FAIL cannot meta 'api-url': key is read-only
-# Export only matching keys.
-pda export --key "a*"
-
-# Export only entries whose values contain a URL.
-pda export --value "**https**"
+pda meta api-url --ttl 1h --force
```
-
+
+
+ See also:
+ pda help meta
+
+
-`pda import` to import it all back. By default, each entry is routed to the store it came from (via the `"store"` field in the NDJSON). If no `"store"` field is present, entries go to `store.default_store_name`. Pass a store name as a positional argument to force all entries into one store. Existing keys are updated and new keys are added.
-```bash
-# Entries are routed to their original stores.
-pda import -f my_backup
-# ok restored 5 entries
+#### TTL
-# Force all entries into a specific store by passing a store name.
-pda import mystore -f my_backup
-# ok restored 5 entries into @mystore
+
+
+ ↑ ·
+ pda set,
+ pda meta
+
+
-# Or from stdin.
-pda import < my_backup
+Keys can be given an expiration time. Expired keys are marked for garbage collection and deleted on the next access to the store.
-# Import only matching keys.
-pda import --key "a*" -f my_backup
-
-# Import only entries from matching stores.
-pda import --store "prod*" -f my_backup
-
-# Full replace — drop all existing entries before importing.
-pda import --drop -f my_backup
-```
-
-
-
-You can have as many stores as you want. All the store commands have shorthands, like `mv` to move a key, or `mvs` to move a store.
-```bash
-# Save to a specific store.
-pda set alice@birthdays 11/11/1998
-
-# See which stores have contents.
-pda list-stores
-# Keys Size Store
-# 2 1.8k @birthdays
-# 12 4.2k @store
-
-# Just the names.
-pda list-stores --short
-# @birthdays
-# @store
-
-# Check out a specific store.
-pda ls @birthdays
-# Store Key Value
-# birthdays alice 11/11/1998
-# birthdays bob 05/12/1980
-
-# Export it.
-pda export birthdays > friends_birthdays
-
-# Import it.
-pda import birthdays < friends_birthdays
-
-# Rename it.
-pda move-store birthdays bdays
-
-# Or copy it.
-pda move-store birthdays bdays --copy
-
-# --safe to skip if the destination already exists.
-pda move-store birthdays bdays --safe
-
-# Delete it.
-pda remove-store birthdays
-
-# --yes/-y to skip confirmation prompts on delete or overwrite.
-pda remove-store birthdays -y
-```
-
-
-
-### Git
-
-pda! supports automatic version control backed by Git, either in a local-only repository or by initialising from a remote repository.
-
-`pda init` will initialise the version control system.
-```bash
-# Initialise an empty pda! repository.
-pda init
-
-# Or clone an existing one.
-pda init https://github.com/llywelwyn/my-repository
-
-# --clean to replace your (existing) local repo with a new one.
-pda init --clean
-```
-
-
-
-`pda sync` conducts a best-effort syncing of your local data with your Git repository. Any time you swap machine or know you've made changes outside of `pda!` itself, I recommend syncing.
-
-If you're ahead of your Git repo, syncing will add your changes, commit them, and push to remote if a remote is set. If you use multiple devices or otherwise end up behind your Git repo, syncing will detect this and give you a prompt: either stash your local changes and pull the latest commit from version control, or abort and fix the issue manually.
+Set a TTL at creation time with [`pda set --ttl`](#setting):
```bash
-# Sync with Git
-pda sync
-
-# With a custom commit message.
-pda sync -m "added production credentials"
-```
-
-`pda!` supports some automation via its config. There are options for `git.auto_commit`, `git.auto_fetch`, and `git.auto_push`. Any of these operations will slow down `pda!` because it means versioning with every change, but it does effectively guarantee never managing to desync oneself and requiring manual fixes, and reduces the frequency with which one will need to manually run the sync command.
-
-Auto-commit will commit changes immediately to the local Git repository any time `pda!` data is changed. Auto-fetch will fetch before committing any changes, but incurs a significant slowdown in operations simply due to the time a fetch takes. Auto-push will automatically push committed changes to the remote repository, if one is set.
-
-If auto-commit is set to false, auto-fetch and auto-push will do nothing. They can be considered to be additional steps taken during the commit process.
-
-Running `pda sync` manually will always fetch, commit, and push - or if behind it will fetch, stash, and pull - regardless of config.
-
-My general recommendation would be to enable `git.auto_commit`, and to run a manual `pda sync` any time you're preparing to switch machines, or loading up a new one.
-
-
-
-### Templates
-
-Values support effectively all of Go's `text/template` syntax. Templates are evaluated on `pda get`.
-
-`text/template` is a Turing-complete templating library that supports most of what you'd expect in a scripting language. Actions are given with ``{{ action }}`` syntax and support pipelines and nested templates, along with a lot more. I recommend reading the documentation if you want to do anything more complicated than described here.
-
-To fit `text/template` nicely into this tool, pda has a sparse set of additional functions built-in. For example, `default` values, `enum`s, `require`d values, `time`, `lists`, arbitrary `shell` execution, and getting other `pda` keys (recursively!). These same functions are also available in `git.default_commit_message` templates, along with `summary` which returns the action that triggered the commit (e.g. "set foo", "removed bar").
-
-Below is more detail on the extra functions added by this tool.
-
-
-
-`{{ .BASIC }}` substitution
-```bash
-pda set greeting "Hello, {{ .NAME }}"
-pda get greeting NAME="Alice"
-# Hello, Alice
-```
-
-
-
-`default` sets a default value.
-```bash
-pda set greeting "Hello, {{ default "World" .NAME }}"
-pda get greeting
-# Hello, World
-pda get greeting NAME="Bob"
-# Hello, Bob
-```
-
-
-
-`require` errors if missing.
-```bash
-pda set file "{{ require .FILE }}"
-pda get file
-# FAIL cannot get 'file': ...required value is missing or empty
-```
-
-
-
-`env` reads from environment variables.
-```bash
-pda set my_name "{{ env "USER" }}"
-pda get my_name
-# llywelwyn
-```
-
-
-
-`time` returns the current UTC time in RFC3339 format.
-```bash
-pda set note "Created at {{ time }}"
-pda get note
-# Created at 2025-01-15T12:00:00Z
-```
-
-
-
-`enum` restricts acceptable values.
-```bash
-pda set level "Log level: {{ enum .LEVEL "info" "warn" "error" }}"
-pda get level LEVEL=info
-# Log level: info
-pda get level LEVEL=debug
-# FAIL cannot get 'level': ...invalid value 'debug', allowed: [info warn error]
-```
-
-
-
-`int` to parse as an integer.
-```bash
-pda set number "{{ int .N }}"
-pda get number N=3
-# 3
-
-# Use it in a loop.
-pda set meows "{{ range int .COUNT }}meow! {{ end }}"
-pda get meows COUNT=4
-# meow! meow! meow! meow!
-```
-
-
-
-`list` to parse CSV as a list.
-```bash
-pda set names "{{ range list .NAMES }}Hi {{.}}. {{ end }}"
-pda get names NAMES=Bob,Alice
-# Hi Bob. Hi Alice.
-```
-
-
-
-`shell` executes a command and returns stdout.
-```bash
-pda set rev '{{ shell "git rev-parse --short HEAD" }}'
-pda get rev
-# a1b2c3d
-
-pda set today '{{ shell "date +%Y-%m-%d" }}'
-pda get today
-# 2025-06-15
-```
-
-
-
-`pda` gets another key.
-```bash
-pda set base_url "https://api.example.com"
-pda set endpoint '{{ pda "base_url" }}/users/{{ require .ID }}'
-pda get endpoint ID=42
-# https://api.example.com/users/42
-
-# Cross-store references work too.
-pda set host@urls "https://example.com"
-pda set api '{{ pda "host@urls" }}/api'
-pda get api
-# https://example.com/api
-```
-
-
-
-pass `no-template` to output literally without templating.
-```bash
-pda set hello "{{ if .MORNING }}Good morning.{{ end }}"
-pda get hello MORNING=1
-# Good morning.
-pda get hello --no-template
-# {{ if .MORNING }}Good morning.{{ end }}
-```
-
-
-
-### Filtering
-
-`--key`/`-k`, `--value`/`-v`, and `--store`/`-s` can be used as filters with glob support. `gobwas/glob` is used for matching. All three flags are repeatable, with results matching one-or-more of the patterns passed per flag. When multiple flags are combined, results must satisfy all of them (AND across flags, OR within the same flag).
-
-`--key`, `--value`, and `--store` filters work with `list`, `export`, `import`, and `remove`. `--value` is not available on `import` or `remove`.
-
-
-
-`*` wildcards a word or series of characters, stopping at separator boundaries (the default separators are `/-_.@:` and space).
-```bash
-pda ls
-# cat
-# dog
-# cog
-# mouse hotdog
-# mouse house
-# foo.bar.baz
-
-pda ls --key "*"
-# cat
-# dog
-# cog
-
-pda ls --key "* *"
-# mouse hotdog
-# mouse house
-
-pda ls --key "foo.*.baz"
-# foo.bar.baz
-```
-
-
-
-`**` super-wildcards ignore word boundaries.
-```bash
-pda ls --key "foo**"
-# foo.bar.baz
-
-pda ls --key "**g"
-# dog
-# cog
-# mouse hotdog
-```
-
-
-
-`?` wildcards a single letter.
-```bash
-pda ls --key "?og"
-# dog
-# cog
-# frog --> fail
-# dogs --> fail
-```
-
-
-
-`[abc]` must match one of the characters in the brackets.
-```bash
-pda ls --key "[dc]og"
-# dog
-# cog
-# bog --> fail
-
-# Can be negated with '!'
-pda ls --key "[!dc]og"
-# dog --> fail
-# cog --> fail
-# bog
-```
-
-
-
-`[a-c]` must fall within the range given in the brackets.
-```bash
-pda ls --key "[a-g]ag"
-# bag
-# gag
-# wag --> fail
-
-# Can be negated with '!'
-pda ls --key "[!a-g]ag"
-# bag --> fail
-# gag --> fail
-# wag
-
-pda ls --key "19[90-99]"
-# 1991
-# 1992
-# 2001 --> fail
-# 1988 --> fail
-```
-
-
-
-`--value` filters by value content using the same glob syntax.
-```bash
-pda ls --value "**localhost**"
-# Key Value
-# db-url postgres://localhost:5432
-
-# Combine key and value filters.
-pda ls --key "db*" --value "**localhost**"
-# Key Value
-# db-url postgres://localhost:5432
-
-# Multiple --value patterns are OR'd.
-pda ls --value "**world**" --value "42"
-# Key Value
-# greeting hello world
-# number 42
-```
-
-
-
-Globs can be arbitrarily complex, and `--key` can be combined with exact positional args on `rm`.
-```bash
-pda rm cat --key "{mouse,[cd]og}**"
-# ??? remove 'cat'? (y/n)
-# ==> y
-# ??? remove 'mouse trap'? (y/n)
-# ...
-```
-
-Locked (encrypted without an available identity) and non-UTF-8 (binary) entries are silently excluded from `--value` matching.
-
-
-
-### TTL
-
-`ttl` sets an expiration time. Expired keys get marked for garbage collection and will be deleted on the next-run of the store. They wont be accessible.
-```bash
-# Expire after 1 hour
+# expire after 1 hour
pda set session "123" --ttl 1h
-# After 54 minutes and 10 seconds
+# expire after 54 minutes and 10 seconds
pda set session2 "xyz" --ttl 54m10s
```
-
-
-`list` shows expiration in the TTL column by default.
-```bash
-pda ls
-# TTL Key Value
-# 59m30s session 123
-# 51m40s session2 xyz
-```
-
-`export` and `import` persist the expiry date. Expirations will continue ticking down regardless of if they're actively in a store or not - the expiry is just a timestamp, not a timer.
-
-
-
-`meta --ttl` to change or clear the TTL on an existing key.
-```bash
-pda meta session --ttl 2h
-# ok set ttl to 2h session
-
-pda meta session --ttl never
-# ok cleared ttl session
-```
-
-
-
-### Read-only
-
-Keys marked read-only are protected from accidental modification. You can modify a read-only key again by making it `--writable` or by explicitly forcing it with the `--force` flag when trying to make an edit.
+[`pda list`](#listing) shows expiration in the TTL column:
```bash
-# Set a key as read-only at creation time.
-pda set api-url "https://prod.example.com" --readonly
-
-# Or toggle with meta.
-pda meta api-url --readonly
-# ok made readonly api-url
-pda meta api-url --writable
-# ok made writable api-url
-
-# Or alongside an edit.
-pda edit notes --readonly
+❯ pda ls
+ TTL Key Value
+59m30s session 123
+51m40s session2 xyz
```
-
-
-Read-only keys are protected from `set`, `rm`, `mv`, and `edit`. Use `--force` to bypass.
-```bash
-pda set api-url "new value"
-# FAIL cannot set 'api-url': key is read-only
-
-pda set api-url "new value" --force
-# overwrites despite read-only
-
-pda rm api-url --force
-pda mv api-url new-name --force
-
-# Modifying a read-only key's metadata also requires --force.
-pda meta api-url --ttl 1h
-# FAIL cannot meta 'api-url': key is read-only
-pda meta api-url --ttl 1h --force
-# ok set ttl to 1h api-url
-```
-
-
-
-`cp` can copy a read-only key freely (since the source isn't modified), and the copy preserves the read-only flag. Overwriting a read-only destination is blocked without `--force`.
-
-
-
-### Pinned
-
-Pinned keys sort to the top of `list` output, preserving alphabetical order within the pinned and unpinned groups.
+Change or clear the TTL on an existing key with [`pda meta --ttl`](#metadata):
```bash
-# Pin a key at creation time.
-pda set important "remember this" --pin
+❯ pda meta session --ttl 2h
+ ok set ttl to 2h session
-# Or toggle with meta.
-pda meta todo --pin
-# ok pinned todo
-pda meta todo --unpin
-# ok unpinned todo
+❯ pda meta session --ttl never
+ ok cleared ttl session
```
-
+The [`edit`](#editing) command also accepts `--ttl`:
```bash
-pda ls
-# Meta Key Value
-# -w-p important remember this
-# -w-- name Alice
-# -w-- other foo
+pda edit session --ttl 30m
```
-
+[`export`](#import--export) and [`import`](#import--export) preserve the expiry date. Expirations are stored as a timestamp, not a timer — they continue ticking down regardless of whether the key is in an active store or sitting in a backup file.
-### Binary
+
+
+ See also:
+ pda help set,
+ pda help meta
+
+
-Save binary data.
-```bash
-pda set logo < logo.png
-```
+#### Encryption
-
+
+
+ ↑ ·
+ pda set,
+ pda meta,
+ pda identity
+
+
-And `get` it like normal.
-```bash
-pda get logo > output.png
-```
-
-
-
-`list` and `get` will show a summary for binary data on a TTY. If it's being piped somewhere or ran outside of a TTY, it'll output the raw bytes.
-
-`--base64`/`-b` to view binary data as base64 on a TTY.
-```bash
-pda get logo
-# (binary: 4.2 KB, image/png)
-
-pda get logo --base64
-# iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADklEQVQI12...
-```
-
-
-
-`export` encodes binary data as base64.
-```bash
-pda export
-# {"key":"logo","value":"89504E470D0A1A0A0000000D4948445200000001000000010802000000","encoding":"base64"}
-```
-
-
-
-### Encryption
-
-`pda set --encrypt` encrypts values at rest using [age](https://github.com/FiloSottile/age). Values are stored on disk as age ciphertext and decrypted automatically by commands like `get` and `list` when the correct identity file is present. An X25519 identity is generated on first use and saved at `~/.config/pda/identity.txt`.
+[`pda set --encrypt`](#setting) encrypts values at rest using [age](https://github.com/FiloSottile/age). Values are stored on disk as age ciphertext and decrypted automatically by commands like [`get`](#getting) and [`list`](#listing) when the correct identity file is present. An X25519 identity is generated on first use.
```bash
pda set --encrypt api-key "sk-live-abc123"
@@ -859,172 +784,1097 @@ pda set --encrypt api-key "sk-live-abc123"
pda set --encrypt token "ghp_xxxx"
```
-
+[`pda get`](#getting) decrypts automatically:
-`meta --encrypt` and `meta --decrypt` to toggle encryption on an existing key.
```bash
-pda meta api-key --encrypt
-# ok encrypted api-key
-
-pda meta api-key --decrypt
-# ok decrypted api-key
+❯ pda get api-key
+sk-live-abc123
```
-
+Toggle encryption on an existing key with [`pda meta`](#metadata):
-`get` decrypts automatically.
```bash
-pda get api-key
-# sk-live-abc123
+❯ pda meta api-key --encrypt
+ ok encrypted api-key
+
+❯ pda meta api-key --decrypt
+ ok decrypted api-key
```
-
+The on-disk value is ciphertext, so encrypted entries are safe to commit and push with [Git](#git):
-The on-disk value is ciphertext, so encrypted entries are safe to commit and push with Git.
```bash
-pda export
-# {"key":"api-key","value":"YWdlLWVuY3J5cHRpb24u...","encoding":"secret"}
+❯ pda export
+{"key":"api-key","value":"YWdlLWVuY3J5cHRpb24u...","encoding":"secret"}
```
-
+[`mv`](#moving--copying), [`cp`](#moving--copying), and [`import`](#import--export) all preserve encryption, read-only, and pinned flags. Overwriting an encrypted key without `--encrypt` will warn you:
-`mv`, `cp`, and `import` all preserve encryption, read-only, and pinned flags. Overwriting an encrypted key without `--encrypt` will warn you.
```bash
pda cp api-key api-key-backup
# still encrypted
-pda set api-key "oops"
-# WARN overwriting encrypted key 'api-key' as plaintext
-# hint pass --encrypt to keep it encrypted
+❯ pda set api-key "oops"
+WARN overwriting encrypted key 'api-key' as plaintext
+hint pass --encrypt to keep it encrypted
```
-
+If the identity file is missing, encrypted values are inaccessible but not lost. Keys remain visible, and the ciphertext is preserved through reads and writes:
-If the identity file is missing, encrypted values are inaccessible but not lost. Keys are still visible, and the ciphertext is preserved through reads and writes.
```bash
-pda ls
-# Meta Key Value
-# ew-- api-key locked (identity file missing)
+❯ pda ls
+Meta Key Value
+ew-- api-key locked (identity file missing)
-pda get api-key
-# FAIL cannot get 'api-key': secret is locked (identity file missing)
+❯ pda get api-key
+FAIL cannot get 'api-key': secret is locked (identity file missing)
```
-
+All encryption operations can be set as default with `key.always_encrypt` in [config](#config), so every [`pda set`](#setting) automatically encrypts.
+
+
+
+ See also:
+ pda help set,
+ pda help meta,
+ pda help identity
+
+
+
+#### Read-Only
+
+
+
+ ↑ ·
+ pda set,
+ pda meta,
+ pda edit
+
+
+
+Keys marked read-only are protected from accidental modification. You can modify a read-only key again by making it [`--writable`](#metadata) or by explicitly bypassing with [`--force`](#metadata).
+
+Set a key as read-only at creation time:
-`pda identity` to see your public key and identity file path.
```bash
-pda identity
-# ok pubkey age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p
-# ok identity ~/.config/pda/identity.txt
+pda set api-url "https://prod.example.com" --readonly
+```
-# Just the path.
-pda identity --path
-# ~/.config/pda/identity.txt
+Toggle with [`pda meta`](#metadata):
-# Generate a new identity. Errors if one already exists.
+```bash
+❯ pda meta api-url --readonly
+ ok made readonly api-url
+
+❯ pda meta api-url --writable
+ ok made writable api-url
+```
+
+Or alongside an edit:
+
+```bash
+pda edit notes --readonly
+```
+
+Read-only keys are protected from [`set`](#setting), [`rm`](#removing), [`mv`](#moving--copying), and [`edit`](#editing). Use `--force` to bypass:
+
+```bash
+❯ pda set api-url "new value"
+FAIL cannot set 'api-url': key is read-only
+
+pda set api-url "new value" --force
+pda rm api-url --force
+pda mv api-url new-name --force
+```
+
+Modifying a read-only key's metadata also requires `--force` (except for toggling the read-only flag itself, and pin/unpin):
+
+```bash
+❯ pda meta api-url --ttl 1h
+FAIL cannot meta 'api-url': key is read-only
+
+pda meta api-url --ttl 1h --force
+```
+
+[`cp`](#moving--copying) can copy a read-only key freely (since the source isn't modified), and the copy preserves the read-only flag. Overwriting a read-only destination is blocked without `--force`.
+
+
+
+ See also:
+ pda help set,
+ pda help meta,
+ pda help edit
+
+
+
+#### Pinned
+
+
+
+ ↑ ·
+ pda set,
+ pda meta,
+ pda list
+
+
+
+Pinned keys sort to the top of [`pda list`](#listing) output, preserving alphabetical order within the pinned and unpinned groups.
+
+Pin a key at creation time:
+
+```bash
+pda set important "remember this" --pin
+```
+
+Toggle with [`pda meta`](#metadata):
+
+```bash
+❯ pda meta todo --pin
+ ok pinned todo
+
+❯ pda meta todo --unpin
+ ok unpinned todo
+```
+
+```bash
+❯ pda ls
+Meta Key Value
+-w-p important remember this
+-w-- name Alice
+-w-- other foo
+```
+
+
+
+ See also:
+ pda help set,
+ pda help meta
+
+
+
+### Stores
+
+
+
+ ↑ ·
+ pda list-stores,
+ pda move-store,
+ pda remove-store
+
+
+
+You can have as many stores as you want. Stores are created implicitly when you set a key with a `@STORE` suffix. Each store is a separate NDJSON file on disk.
+
+[`pda list-stores`](#stores) (alias: [`lss`](#stores)) shows all stores with key counts and file sizes:
+
+```bash
+❯ pda list-stores
+Keys Size Store
+ 2 1.8k @birthdays
+ 12 4.2k @store
+```
+
+[`--short`](#stores) prints only the names:
+
+```bash
+❯ pda list-stores --short
+@birthdays
+@store
+```
+
+Save to a specific store with the `@STORE` syntax:
+
+```bash
+pda set alice@birthdays "11/11/1998"
+```
+
+List a specific store:
+
+```bash
+❯ pda ls @birthdays
+ Store Key Value
+birthdays alice 11/11/1998
+birthdays bob 05/12/1980
+```
+
+[`pda move-store`](#stores) (alias: [`mvs`](#stores)) renames a store:
+
+```bash
+pda move-store birthdays bdays
+```
+
+Copy a store with `--copy`:
+
+```bash
+pda move-store birthdays bdays --copy
+```
+
+[`--safe`](#stores) skips if the destination already exists:
+
+```bash
+pda move-store birthdays bdays --safe
+```
+
+[`pda remove-store`](#stores) (alias: [`rms`](#stores)) deletes a store:
+
+```bash
+pda remove-store birthdays
+```
+
+[`--yes`](#stores) / `-y` skips confirmation prompts:
+
+```bash
+pda remove-store birthdays -y
+```
+
+
+
+ See also:
+ pda help list-stores,
+ pda help move-store,
+ pda help remove-store
+
+
+
+#### Import & Export
+
+
+
+ ↑ ·
+ pda export,
+ pda import
+
+
+
+[`pda export`](#import--export) exports everything as NDJSON (it's an alias for `list --format ndjson`):
+
+```bash
+pda export > my_backup
+```
+
+Filter exports with [`--key`](#filtering), [`--value`](#filtering), and [`--store`](#filtering):
+
+```bash
+# export only matching keys
+pda export --key "a*"
+
+# export only entries whose values contain a URL
+pda export --value "**https**"
+```
+
+[`pda import`](#import--export) restores entries from an NDJSON dump. By default, each entry is routed to the store it came from (via the `"store"` field in the NDJSON). If no `"store"` field is present, entries go to `store.default_store_name`.
+
+```bash
+# entries are routed to their original stores
+pda import -f my_backup
+# ok restored 5 entries
+```
+
+Pass a store name as a positional argument to force all entries into one store:
+
+```bash
+pda import mystore -f my_backup
+# ok restored 5 entries into @mystore
+```
+
+Read from stdin:
+
+```bash
+pda import < my_backup
+```
+
+Filter imports with [`--key`](#filtering) and [`--store`](#filtering):
+
+```bash
+# import only matching keys
+pda import --key "a*" -f my_backup
+
+# import only entries from matching stores
+pda import --store "prod*" -f my_backup
+```
+
+[`--drop`](#import--export) does a full replace — drops all existing entries before importing:
+
+```bash
+pda import --drop -f my_backup
+```
+
+[`--interactive`](#import--export) / `-i` prompts before overwriting existing keys.
+
+[`export`](#import--export) encodes [binary data](#binary-data) as base64. [Encryption](#encryption), [read-only](#read-only), [pinned](#pinned) flags, and [TTL](#ttl) are all preserved through export and import.
+
+
+
+ See also:
+ pda help export,
+ pda help import
+
+
+
+### Templates
+
+
+
+ ↑ ·
+ pda get,
+ pda run
+
+
+
+Values support Go's [`text/template`](https://pkg.go.dev/text/template) syntax. Templates are evaluated on [`pda get`](#getting) and [`pda run`](#running).
+
+`text/template` is a Turing-complete templating library that supports pipelines, nested templates, conditionals, loops, and more. Actions are given with `{{ action }}` syntax. To fit `text/template` into a CLI key-value tool, `pda!` adds a small set of built-in functions on top of the standard library.
+
+These same functions are also available in `git.default_commit_message` templates, along with `summary` which returns the action that triggered the commit (e.g. "set foo", "removed bar").
+
+#### Basic Substitution
+
+
+
+ ↑ ·
+ Templates,
+ pda get
+
+
+
+Template variables are substituted from `KEY=VALUE` arguments passed to [`pda get`](#getting):
+
+```bash
+pda set greeting "Hello, {{ .NAME }}"
+
+❯ pda get greeting NAME="Alice"
+Hello, Alice
+```
+
+#### `default`
+
+
+
+ ↑ ·
+ Templates
+
+
+
+`default` sets a fallback value when a variable is missing or empty:
+
+```bash
+pda set greeting "Hello, {{ default "World" .NAME }}"
+
+❯ pda get greeting
+Hello, World
+
+❯ pda get greeting NAME="Bob"
+Hello, Bob
+```
+
+#### `require`
+
+
+
+ ↑ ·
+ Templates
+
+
+
+`require` errors if the variable is missing or empty:
+
+```bash
+pda set file "{{ require .FILE }}"
+
+❯ pda get file
+FAIL cannot get 'file': ...required value is missing or empty
+```
+
+#### `env`
+
+
+
+ ↑ ·
+ Templates
+
+
+
+`env` reads from environment variables:
+
+```bash
+pda set my_name "{{ env "USER" }}"
+
+❯ pda get my_name
+llywelwyn
+```
+
+#### `time`
+
+
+
+ ↑ ·
+ Templates
+
+
+
+`time` returns the current UTC time in RFC3339 format:
+
+```bash
+pda set note "Created at {{ time }}"
+
+❯ pda get note
+Created at 2025-01-15T12:00:00Z
+```
+
+#### `enum`
+
+
+
+ ↑ ·
+ Templates
+
+
+
+`enum` restricts a variable to a set of acceptable values:
+
+```bash
+pda set level "Log level: {{ enum .LEVEL "info" "warn" "error" }}"
+
+❯ pda get level LEVEL=info
+Log level: info
+
+❯ pda get level LEVEL=debug
+FAIL cannot get 'level': ...invalid value 'debug', allowed: [info warn error]
+```
+
+#### `int`
+
+
+
+ ↑ ·
+ Templates
+
+
+
+`int` parses a variable as an integer, useful for loops and arithmetic:
+
+```bash
+pda set number "{{ int .N }}"
+
+❯ pda get number N=3
+3
+```
+
+Use it in a range loop:
+
+```bash
+pda set meows "{{ range int .COUNT }}meow! {{ end }}"
+
+❯ pda get meows COUNT=4
+meow! meow! meow! meow!
+```
+
+#### `list`
+
+
+
+ ↑ ·
+ Templates
+
+
+
+`list` parses a comma-separated string into a list for iteration:
+
+```bash
+pda set names "{{ range list .NAMES }}Hi {{.}}. {{ end }}"
+
+❯ pda get names NAMES=Bob,Alice
+Hi Bob. Hi Alice.
+```
+
+#### `shell`
+
+
+
+ ↑ ·
+ Templates
+
+
+
+`shell` executes a command and returns its stdout:
+
+```bash
+pda set rev '{{ shell "git rev-parse --short HEAD" }}'
+
+❯ pda get rev
+a1b2c3d
+```
+
+```bash
+pda set today '{{ shell "date +%Y-%m-%d" }}'
+
+❯ pda get today
+2025-06-15
+```
+
+#### `pda` (Recursive)
+
+
+
+ ↑ ·
+ Templates
+
+
+
+`pda` gets another key's value, enabling recursive composition:
+
+```bash
+pda set base_url "https://api.example.com"
+pda set endpoint '{{ pda "base_url" }}/users/{{ require .ID }}'
+
+❯ pda get endpoint ID=42
+https://api.example.com/users/42
+```
+
+Cross-store references work too:
+
+```bash
+pda set host@urls "https://example.com"
+pda set api '{{ pda "host@urls" }}/api'
+
+❯ pda get api
+https://example.com/api
+```
+
+#### `no-template`
+
+
+
+ ↑ ·
+ pda get
+
+
+
+Pass [`--no-template`](#getting) to [`pda get`](#getting) to output the raw value without evaluating templates:
+
+```bash
+pda set hello "{{ if .MORNING }}Good morning.{{ end }}"
+
+❯ pda get hello MORNING=1
+Good morning.
+
+❯ pda get hello --no-template
+{{ if .MORNING }}Good morning.{{ end }}
+```
+
+
+
+ See also:
+ pda help get,
+ pda help set
+
+
+
+### Filtering
+
+
+
+ ↑ ·
+ pda list,
+ pda remove,
+ pda export,
+ pda import
+
+
+
+[`--key`](#filtering) / `-k`, [`--value`](#filtering) / `-v`, and [`--store`](#filtering) / `-s` filter entries with glob support. All three flags are repeatable, with results matching one-or-more of the patterns per flag. When multiple flags are combined, results must satisfy all of them (AND across flags, OR within the same flag).
+
+These filters work with [`list`](#listing), [`export`](#import--export), [`import`](#import--export), and [`remove`](#removing). [`--value`](#filtering) is not available on [`import`](#import--export) or [`remove`](#removing).
+
+[`gobwas/glob`](https://github.com/gobwas/glob) is used for matching. The default separators are `/-_.@:` and space.
+
+#### Glob Patterns
+
+
+
+ ↑ ·
+ Filtering
+
+
+
+`*` wildcards a word or series of characters, stopping at separator boundaries:
+
+```bash
+❯ pda ls
+cat
+dog
+cog
+mouse hotdog
+mouse house
+foo.bar.baz
+
+pda ls --key "*"
+# cat, dog, cog (single-segment keys only)
+
+pda ls --key "* *"
+# mouse hotdog, mouse house
+
+pda ls --key "foo.*.baz"
+# foo.bar.baz
+```
+
+`**` super-wildcards ignore word boundaries:
+
+```bash
+pda ls --key "foo**"
+# foo.bar.baz
+
+pda ls --key "**g"
+# dog, cog, mouse hotdog
+```
+
+`?` matches a single character:
+
+```bash
+pda ls --key "?og"
+# dog, cog
+```
+
+`[abc]` matches one of the characters in the brackets:
+
+```bash
+pda ls --key "[dc]og"
+# dog, cog
+
+# negate with '!'
+pda ls --key "[!dc]og"
+# bog (if it exists)
+```
+
+`[a-c]` matches a range:
+
+```bash
+pda ls --key "[a-g]ag"
+# bag, gag
+
+pda ls --key "[!a-g]ag"
+# wag
+```
+
+#### Filtering by Key
+
+
+
+ ↑ ·
+ Filtering,
+ pda list
+
+
+
+[`--key`](#filtering) / `-k` filters entries by key name:
+
+```bash
+pda ls --key "db*"
+pda ls --key "session*" --key "token*"
+```
+
+Multiple `--key` patterns are OR'd — an entry matches if it matches any of them.
+
+#### Filtering by Value
+
+
+
+ ↑ ·
+ Filtering,
+ pda list
+
+
+
+[`--value`](#filtering) / `-v` filters by value content using the same glob syntax:
+
+```bash
+❯ pda ls --value "**localhost**"
+Key Value
+db-url postgres://localhost:5432
+```
+
+Multiple `--value` patterns are OR'd:
+
+```bash
+❯ pda ls --value "**world**" --value "42"
+Key Value
+greeting hello world
+number 42
+```
+
+Locked (encrypted without an available identity) and non-UTF-8 (binary) entries are silently excluded from `--value` matching.
+
+#### Filtering by Store
+
+
+
+ ↑ ·
+ Filtering,
+ pda list
+
+
+
+[`--store`](#filtering) / `-s` filters by store name:
+
+```bash
+pda ls --store "prod*"
+pda export --store "dev*"
+```
+
+#### Combining Filters
+
+
+
+ ↑ ·
+ Filtering
+
+
+
+Combine key, value, and store filters. Results must match all flags (AND), with OR within each flag:
+
+```bash
+pda ls --key "db*" --value "**localhost**"
+```
+
+Globs can be arbitrarily complex, and [`--key`](#filtering) can be combined with exact positional args on [`rm`](#removing):
+
+```bash
+pda rm cat --key "{mouse,[cd]og}**"
+# ??? remove 'cat'? (y/n)
+# ==> y
+# ??? remove 'mouse trap'? (y/n)
+# ...
+```
+
+
+
+ See also:
+ pda help list,
+ pda help remove
+
+
+
+### Binary Data
+
+
+
+ ↑ ·
+ pda set,
+ pda get,
+ pda list
+
+
+
+`pda!` supports all binary data. Save it with [`pda set`](#setting):
+
+```bash
+pda set logo < logo.png
+pda set logo -f logo.png
+```
+
+And retrieve it with [`pda get`](#getting):
+
+```bash
+pda get logo > output.png
+```
+
+On a TTY, [`get`](#getting) and [`list`](#listing) show a summary for binary data. If piped or run outside of a TTY, raw bytes are output:
+
+```bash
+❯ pda get logo
+(binary: 4.2 KB, image/png)
+```
+
+[`--base64`](#getting) / `-b` views binary data as base64:
+
+```bash
+❯ pda get logo --base64
+iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADklEQVQI12...
+```
+
+[`pda export`](#import--export) encodes binary data as base64 in the NDJSON:
+
+```bash
+❯ pda export
+{"key":"logo","value":"89504E470D0A1A0A0000000D4948445200000001000000010802000000","encoding":"base64"}
+```
+
+[`pda edit`](#editing) presents binary values as base64 for editing and decodes them back on save.
+
+
+
+ See also:
+ pda help set,
+ pda help get
+
+
+
+### Git
+
+
+
+ ↑ ·
+ pda init,
+ pda sync,
+ Config
+
+
+
+`pda!` supports automatic version control backed by Git, either in a local-only repository or by initialising from a remote.
+
+#### Init
+
+
+
+ ↑ ·
+ Git,
+ pda sync
+
+
+
+[`pda init`](#git) initialises version control:
+
+```bash
+# initialise an empty repository
+pda init
+
+# or clone an existing one
+pda init https://github.com/llywelwyn/my-repository
+```
+
+[`--clean`](#git) removes the existing `.git` directory first, useful for reinitialising or switching remotes:
+
+```bash
+pda init --clean
+pda init https://github.com/llywelwyn/my-repository --clean
+```
+
+
+
+ See also:
+ pda help init
+
+
+
+#### Sync
+
+
+
+ ↑ ·
+ Git
+
+
+
+[`pda sync`](#sync) conducts a best-effort sync of your local data with your Git repository. Any time you swap machine or know you've made changes outside of `pda!`, syncing is recommended.
+
+If you're ahead, syncing will commit and push. If you're behind, syncing will detect this and prompt you: either stash local changes and pull, or abort and fix manually.
+
+```bash
+# sync with Git
+pda sync
+
+# with a custom commit message
+pda sync -m "added production credentials"
+```
+
+Running [`pda sync`](#sync) manually will always fetch, commit, and push — or stash and pull if behind — regardless of config.
+
+
+
+ See also:
+ pda help sync
+
+
+
+#### Auto-Commit & Auto-Push
+
+
+
+ ↑ ·
+ Git,
+ Config
+
+
+
+`pda!` supports automation via its [config](#config). There are options for `git.auto_commit`, `git.auto_fetch`, and `git.auto_push`.
+
+**`git.auto_commit`** commits changes immediately to the local Git repository any time data is changed.
+
+**`git.auto_fetch`** fetches before committing any changes. This incurs a noticeable slowdown due to network round-trips.
+
+**`git.auto_push`** automatically pushes committed changes to the remote repository, if one is configured.
+
+If `auto_commit` is false, `auto_fetch` and `auto_push` have no effect. They are additional steps in the commit process.
+
+A recommended setup is to enable `git.auto_commit` and run [`pda sync`](#sync) manually when switching machines.
+
+### Identity
+
+
+
+ ↑ ·
+ pda identity,
+ Encryption
+
+
+
+[`pda identity`](#identity) (alias: [`id`](#identity)) manages the age encryption identity used for [encryption](#encryption).
+
+#### Viewing Identity
+
+
+
+ ↑ ·
+ pda identity
+
+
+
+With no flags, [`pda identity`](#identity) shows your public key, identity file path, and any additional recipients:
+
+```bash
+❯ pda identity
+ ok pubkey age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p
+ ok identity ~/.config/pda/identity.txt
+```
+
+[`--path`](#identity) prints only the identity file path:
+
+```bash
+❯ pda identity --path
+~/.config/pda/identity.txt
+```
+
+#### Creating an Identity
+
+
+
+ ↑ ·
+ pda identity
+
+
+
+An identity is generated automatically the first time you use [`--encrypt`](#encryption). To create one manually:
+
+```bash
pda identity --new
```
-
+[`--new`](#identity) errors if an identity already exists. Delete the file manually to replace it.
+
+#### Recipients
+
+
+
+ ↑ ·
+ pda identity,
+ Encryption
+
+
+
+By default, secrets are encrypted only for your own identity. To encrypt for additional recipients (e.g. a teammate or another device), use [`--add-recipient`](#identity) with their age public key. All existing secrets are automatically re-encrypted for every recipient:
-By default, secrets are encrypted only for your own identity. To encrypt for additional recipients (e.g. a teammate or another device), use `--add-recipient` with their age public key. All existing secrets are automatically re-encrypted for every recipient.
```bash
-# Add a recipient. All secrets are re-encrypted for both keys.
-pda identity --add-recipient age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p
-# ok re-encrypted api-key
-# ok added recipient age1ql3z...
-# ok re-encrypted 1 secret(s)
+❯ pda identity --add-recipient age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p
+ ok re-encrypted api-key
+ ok added recipient age1ql3z...
+ ok re-encrypted 1 secret(s)
+```
-# Remove a recipient. Secrets are re-encrypted without their key.
+Remove a recipient with [`--remove-recipient`](#identity). Secrets are re-encrypted without their key:
+
+```bash
pda identity --remove-recipient age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p
-
-# Additional recipients are shown in the default identity display.
-pda identity
-# ok pubkey age1abc...
-# ok identity ~/.local/share/pda/identity.txt
-# ok recipient age1ql3z...
```
-
-
-### Doctor
-
-`pda doctor` runs a set of health checks of your environment.
+Additional recipients are shown in the default identity display:
```bash
-pda doctor
-# ok pda! 2025.52 Christmas release (linux/amd64)
-# ok OS: Linux 6.18.7-arch1-1
-# ok Go: go1.23.0
-# ok Git: 2.45.0
-# ok Shell: /bin/zsh
-# ok Config: /home/user/.config/pda
-# ok Non-default config:
-# ├── display_ascii_art: false
-# └── git.auto_commit: true
-# ok Data: /home/user/.local/share/pda
-# ok Identity: /home/user/.config/pda/identity.txt
-# ok Git initialised on main
-# ok Git remote configured
-# ok Git in sync with remote
-# ok 3 store(s), 15 key(s), 2 secret(s), 4.2k total size
-# ok No issues found
+❯ pda identity
+ ok pubkey age1abc...
+ ok identity ~/.local/share/pda/identity.txt
+ ok recipient age1ql3z...
```
-
-
-Severity levels are colour-coded: `ok` (green), `WARN` (yellow), and `FAIL` (red). Only `FAIL` produces a non-zero exit code. `WARN` is generally not a problem, but may mean some functionality isn't being made use of, like for example version control not having been initialised yet.
-
-
+
+
+ See also:
+ pda help identity
+
+
### Config
+
+
+ ↑ ·
+ pda config,
+ pda doctor
+
+
+
Config is stored at `~/.config/pda/config.toml` (Linux/macOS) or `%LOCALAPPDATA%/pda/config.toml` (Windows). All values have sensible defaults, so a config file is entirely optional.
-
+#### Config Commands
+
+
+
+ ↑ ·
+ pda config
+
+
+
+[`pda config`](#config) manages configuration without editing files by hand:
-`pda config` manages configuration without editing files by hand.
```bash
-# List all config values and their current settings.
+# list all config values and their current settings
pda config list
-# Get a single value.
-pda config get git.auto_commit
-# false
+# get a single value
+❯ pda config get git.auto_commit
+false
-# Set a value. Validated before saving.
+# set a value (validated before saving)
pda config set git.auto_commit true
-# Open in $EDITOR. Validated on save.
+# open in $EDITOR (validated on save)
pda config edit
-# Print the config file path.
+# print the config file path
pda config path
-# Generate a fresh default config file.
+# generate a fresh default config file
pda config init
-# Overwrite an existing config with defaults.
+# overwrite an existing config with defaults
pda config init --new
-# Update config: migrate deprecated keys and fill missing defaults.
+# update config: migrate deprecated keys and fill missing defaults
pda config init --update
```
-
+[`pda doctor`](#doctor) will warn about unrecognised keys (typos, removed options) and show any non-default values, so it doubles as a config audit.
-`pda doctor` will warn about unrecognised keys (typos, removed options) and show any non-default values, so it doubles as a config audit.
-
-
+
+
+ See also:
+ pda help config
+
+
#### Example config.toml
+
+
+ ↑ ·
+ Config
+
+
+
All values below are the defaults. A missing config file or missing keys will use these values.
```toml
# display ascii header in long root and version commands
-display_ascii_art = true
+display_ascii_art = true
[key]
# prompt y/n before deleting keys
@@ -1068,55 +1918,116 @@ auto_push = false
default_commit_message = "{{ summary }} {{ time }}"
```
-
-
### Environment
-`PDA_CONFIG` overrides the config directory. pda! will look for `config.toml` in this directory.
+
+
+ ↑ ·
+ Config,
+ pda doctor
+
+
+
+`PDA_CONFIG` overrides the config directory. `pda!` will look for `config.toml` in this directory:
+
```bash
PDA_CONFIG=/tmp/config/ pda set key value
```
-
-
-`PDA_DATA` overrides the data storage directory.
-
-Default locations:
-- Linux: `~/.local/share/pda/`
-- macOS: `~/Library/Application Support/pda/`
-- Windows: `%LOCALAPPDATA%/pda/`
+`PDA_DATA` overrides the data storage directory:
```bash
PDA_DATA=/tmp/stores pda set key value
```
-
+Default data locations:
+- Linux: `~/.local/share/pda/`
+- macOS: `~/Library/Application Support/pda/`
+- Windows: `%LOCALAPPDATA%/pda/`
+
+`EDITOR` is used by [`pda edit`](#editing) and [`pda config edit`](#config) to open values in a text editor. Must be set for these commands to work:
-`EDITOR` is used by `pda edit` and `pda config edit` to open values in a text editor. Must be set for these commands to work.
```bash
EDITOR=nvim pda edit mykey
```
-
+`SHELL` is used by [`pda run`](#running) (or [`pda get --run`](#getting)) for command execution. Falls back to `/bin/sh` if unset:
-`SHELL` is used by `pda run` (or `pda get --run`) for command execution. Falls back to `/bin/sh` if unset.
```bash
pda run script
```
-
+### Doctor
-### Version
+
+
+ ↑ ·
+ Config,
+ Environment
+
+
-`pda!` uses calendar versioning: `YYYY.WW`. ASCII art can be permanently disabled with `display_ascii_art = false` in config.
+[`pda doctor`](#doctor) runs a set of health checks of your environment:
```bash
-# Display the full version output.
-pda version
-
-# Or just the release.
-pda version --short
-# pda! 2025.47 release
+❯ pda doctor
+ ok pda! 2025.52 Christmas release (linux/amd64)
+ ok OS: Linux 6.18.7-arch1-1
+ ok Go: go1.23.0
+ ok Git: 2.45.0
+ ok Shell: /bin/zsh
+ ok Config: /home/user/.config/pda
+ ok Non-default config:
+ ├── display_ascii_art: false
+ └── git.auto_commit: true
+ ok Data: /home/user/.local/share/pda
+ ok Identity: /home/user/.config/pda/identity.txt
+ ok Git initialised on main
+ ok Git remote configured
+ ok Git in sync with remote
+ ok 3 store(s), 15 key(s), 2 secret(s), 4.2k total size
+ ok No issues found
```
-
+Severity levels are colour-coded: `ok` (green), `WARN` (yellow), and `FAIL` (red). Only `FAIL` produces a non-zero exit code. `WARN` is generally not a problem, but may mean some functionality isn't being made use of, like version control not having been initialised yet.
+
+
+
+ See also:
+ pda help doctor
+
+
+
+### Help & Version
+
+
+
+ ↑
+
+
+
+```bash
+# help for any command
+pda help set
+pda help list
+pda help config
+
+# display the full version output
+pda version
+
+# or just the release
+❯ pda version --short
+pda! 2025.52 Christmas release
+```
+
+`pda!` uses calendar versioning: `YYYY.WW`. ASCII art can be permanently disabled with `display_ascii_art = false` in [config](#config).
+
+### License
+
+
+
+ ↑
+
+
+
+MIT — see [LICENSE](LICENSE).