| .github/workflows | ||
| cmd | ||
| docs | ||
| testdata | ||
| vhs | ||
| .gitignore | ||
| go.mod | ||
| go.sum | ||
| LICENSE | ||
| main.go | ||
| main_test.go | ||
| README.md | ||
pda! is a command-line key-value store tool with:
- templates supporting arbitrary shell execution, conditionals, loops, more,
- encryption at rest using age,
- Git-backed version control with automatic syncing,
- search and filtering by key, value, or store,
- plaintext exports in 7 different formats,
- support for all binary data,
- expiring keys with a time-to-live,
- read-only keys and pinned entries,
- built-in diagnostics and configuration,
and more, written in pure Go, and inspired by skate and nb.
pda stores key-value pairs natively as newline-delimited JSON files. pda list outputs tabular data by default, but also supports CSV, TSV, Markdown and HTML tables, JSON, and raw NDJSON. Everything is in plaintext to make version control easy, and to avoid tying anybody to using this tool forever.
Git versioning can be initiated with pda init, and varying levels of automation can be toggled via the config: git.autocommit, git.autofetch, and git.autopush. Running Git operations on every change can be slow, but a commit is fast. A happy middle-ground is enabling git.autocommit and doing the rest manually via pda sync when changing devices.
Templates are a primary feature, enabling values to make use of substitutions, environment variables, arbitrary shell execution, cross-references to other keys, and more.
Installation
↑ 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 is needed for compiling the
pdabinary. - git enhances
pdawith version control.
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.
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 users can install and update pda from the aur 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.
# 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 and so comes with shell completions for bash, zsh, fish, and powershell.
# 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
Most commands have aliases and flags. Run pda help [command] to see them.
Key commands
↑ ·
Setting,
Getting,
Running,
Listing,
Editing,
Moving and Copying,
Removing
Use of pda revolves around creating keys with pda set and later retrieving them with pda get. 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 and pda meta for content or metadata editing respectively, and can be listed with pda list. 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 and pda run.
Setting
pda set (alias: s) creates a key-value pair, and requires a key and a value as inputs. The first argument given will always be used to determine the key (and store). Values can come from arguments, stdin, or a file.
# create a key-value pair
pda set name "Alice"
# create a key-value pair in the "Favourites" store
pda set movie@favourites "The Road"
# create a key-value pair with piped input
echo "Bob" | pda set name
# create a key-value pair with redirection
pda set example < silmarillion.txt
# create a pinned key-value pair from a file
pda set --pin example --file example.md
# 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.
# prevent ever overwriting an existing key
pda set name "Bob" --safe
# 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 or pda meta.
# create a readonly key-value pair
pda set repo "https://github.com/llywelwyn/pda" --readonly
# force-overwrite a readonly key-value pair
pda set dog "A four-legged mammal that isn't a cat." --force
Getting
pda get (alias: g) can be used to retrieve a key's value, and takes one argument: the desired key. The value is output to stdout. Templates are evaluated at retrieval time unless opted-out via the no-template flag.
# get the value of a key
❯ pda get name
Alice
As mentioned in 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, 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.
# 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.
# 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 will resolve templates in the stored key at run-time. This can be prevented with the no-template flag.
# 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 --no-template
{{ env "USER" }}
Running
↑ ·
pda get --run ·
pda run
pda run 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`.
Running takes one argument: the key. pda run and pda get --run are functionally equivalent.
# 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.
❯ pda run my_script
Hello, world.
Templates are fully resolved before any shell execution happens.
# 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 (alias: ls) displays your key-value pairs. 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.
-b, --base64 view binary data as base64 -c, --count print only the count of matching entries
pda list (alias: ls) displays stored key-value pairs with pinned keys first, followed by alphabetical order. Default behaviour is to list metadata, size, time-to-live, store, the key, and value as a table. The order and visibility of every column can be toggled either via list.default_columns in the config or via one-off flags.
It accepts one or zero arguments. If no argument is passed, with a default configuration pda list will display all keys from all stores, but this behaviour can be toggled to instead display only keys from the default store. If a store name is passed as an argument, only the keys contained within that store will be listed.
If list.always_show_all_stores is toggled off, the behaviour can be enabled on an individual basis by passing all.
# list all store contents
❯ pda list
Meta Size TTL Store Key Value
-w-p 5 - todos todo don't forget this
---- 23 - store url https://example.com
-w-- 5 - me name Alice
# list only the contents of the "todos" store
❯ pda list todos
Meta Size TTL Store Key Value
-w-p 5 - todos todo don't forget this
# list all store contents, but without Meta, Size, or TTL
❯ pda list --no-ttl --no-header --no-size
Store Key Value
todos todo don't forget this
store url https://example.com
me name Alice
# count the number of entries for a given query
❯ pda list --count
3
When listing, glob patterns can be used to further filter by key, store, or value.
# list all store contents beginning with "https"
pda ls --value "https**"
# list all store contents beginning with "https" with "db" in the key
pda ls --value "https**" --key "**db**"
The standard tabular output of pda list can be swapped out for tab-separated values, comma-separated values, a markdown table, a HTML table, newline-delimited JSON, or JSON. Newline-delimited JSON is the native storage format of a pda store.
# list all store contents as comma-separated values
❯ pda ls --format csv
Meta,Size,TTL,Store,Key,Value
-w--,5,-,store,name,Alice
# list all store contents as JSON
❯ pda ls --format json
[{"key":"name","value":"Alice","encoding":"text","store":"store"}]
By default, long values are truncated to fit the terminal, but this behaviour can be toggled via the config or by passing full. The setting is list.always_show_full_values.
❯ 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
As with getting, non-UTF8 data in lists will be substituted for a summary rather than displaying raw bytes. This can be changed out for a base64 representation by passing base64.
Editing
pda edit (alias: e) opens a key's value in your $EDITOR. If the key doesn't exist, an empty file is opened, and saving non-empty content creates it.
When editing a key, metadata-altering flags can be passed in the same operation. These alterations will only take place if the edit is finalised by saving.
# edit an existing key or create a new one
pda edit name
# edit a key and pin it
pda edit name --pin
# edit a key and give it an expiration, and encrypt it
pda edit secret_stuff --ttl 1h --encrypt
# edit a key and make it readonly
pda edit do_not_change --readonly
Most $EDITOR will add a trailing newline on saving a file. These are stripped by default, but this behaviour can be toggled via edit.always_preserve_newline in the config or as a one-off by passing preserve-newline.
# edit a key and preserve the automatic $EDITOR newline
pda edit example --preserve-newline
Binary values will be presented as base64 for editing and will be decoded back to raw bytes on save. Read-only keys require being made writable before they can be edited, or force can be explicitly passed.
Moving & Copying
To move (or rename) a key, pda move (alias: mv) can be used. To copy a key, pda move --copy (alias: cp) can be used. With both of these operations, all metadata is preserved.
# rename a key
❯ pda move name name2
ok renamed name to name2
# move a key across stores
❯ pda mv name name@some_other_store
ok renamed name to name@some_other_store
# copy a key
❯ pda cp name name2
ok copied name to name2
Accidental overwrites have a few ways of being prevented. safe exists for moving and copying as it does for all changeful commands, skipping an operation if a destination already exists that would be overwritten. A read-only key will also prevent being moved or overwritten unless force is explicitly passed.
Additionally, interactive being passed or key.always_prompt_overwrite being enabled in the config will cause a yes-no prompt to be presented if a key is going to be overwritten. Inversely, prompts can always be skipped by passing yes.
# move a key safely
❯ pda mv name name2 --safe
# info skipped 'name2': already exists
# move a key interactively
❯ pda mv name name2 --safe
??? overwrite 'name2'? (y/n)
>>> y
# move a key and skip all warning prompts
❯ pda mv name name2 --yes
ok renamed name to name2
Removing
pda remove (alias: rm) deletes one or more keys.
pda rm kitty
Remove multiple keys at once:
pda rm kitty dog@animals
Mix exact keys with glob patterns using --key:
pda set cog "cogs"
pda set dog "doggy"
pda set kitty "cat"
pda rm kitty --key "?og"
Filter by store with --store / -s and by value with --value / -v:
pda rm --store "temp*" --key "session*"
--interactive / -i prompts before each deletion (or set key.always_prompt_delete in config):
pda rm kitty -i
# ??? remove 'kitty'? (y/n)
# ==> y
Glob-matched deletions prompt by default (configurable with key.always_prompt_glob_delete).
--yes / -y auto-accepts all confirmation prompts:
pda rm kitty -y
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
Metadata
↑ ·
pda meta,
TTL,
Encryption,
Read-Only,
Pinned
pda meta views or modifies metadata for a key without changing its value. With no flags, it displays the key's current metadata:
❯ pda meta session
key: session@store
secret: false
writable: true
pinned: false
expires: 59m30s
Pass flags to modify: --ttl, --encrypt / --decrypt, --readonly / --writable, --pin / --unpin.
Multiple metadata changes can be combined in one call:
pda meta session --ttl 2h --encrypt --pin
Modifying a read-only key's metadata requires --force (except for toggling the read-only flag itself, and pin/unpin):
❯ pda meta api-url --ttl 1h
FAIL cannot meta 'api-url': key is read-only
pda meta api-url --ttl 1h --force
TTL
↑ ·
pda help set,
pda help meta
Keys can be given an expiration time. Expired keys are marked for garbage collection and deleted on the next access to the store.
Set a TTL at creation time with pda set --ttl:
# expire after 1 hour
pda set session "123" --ttl 1h
# expire after 54 minutes and 10 seconds
pda set session2 "xyz" --ttl 54m10s
pda list shows expiration in the TTL column:
❯ pda ls
TTL Key Value
59m30s session 123
51m40s session2 xyz
Change or clear the TTL on an existing key with pda meta --ttl:
❯ pda meta session --ttl 2h
ok set ttl to 2h session
❯ pda meta session --ttl never
ok cleared ttl session
The edit command also accepts --ttl:
pda edit session --ttl 30m
export and import 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.
Encryption
↑ ·
pda help set,
pda help meta,
pda help identity
pda set --encrypt encrypts values at rest using 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.
pda set --encrypt api-key "sk-live-abc123"
# ok created identity at ~/.config/pda/identity.txt
pda set --encrypt token "ghp_xxxx"
pda get decrypts automatically:
❯ pda get api-key
sk-live-abc123
Toggle encryption on an existing key with pda meta:
❯ 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:
❯ pda export
{"key":"api-key","value":"YWdlLWVuY3J5cHRpb24u...","encoding":"secret"}
mv, cp, and import all preserve encryption, read-only, and pinned flags. Overwriting an encrypted key without --encrypt will warn you:
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
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:
❯ 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)
All encryption operations can be set as default with key.always_encrypt in config, so every pda set automatically encrypts.
Read-Only
↑ ·
pda help set,
pda help meta
Keys marked read-only are protected from accidental modification. You can modify a read-only key again by making it --writable or by explicitly bypassing with --force.
Set a key as read-only at creation time:
pda set api-url "https://prod.example.com" --readonly
Toggle with pda 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
Read-only keys are protected from set, rm, mv, and edit. Use --force to bypass:
❯ 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):
❯ pda meta api-url --ttl 1h
FAIL cannot meta 'api-url': key is read-only
pda meta api-url --ttl 1h --force
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
↑ ·
pda help set,
pda help meta
Pinned keys sort to the top of pda list output, preserving alphabetical order within the pinned and unpinned groups.
Pin a key at creation time:
pda set important "remember this" --pin
Toggle with pda meta:
❯ pda meta todo --pin
ok pinned todo
❯ pda meta todo --unpin
ok unpinned todo
❯ pda ls
Meta Key Value
-w-p important remember this
-w-- name Alice
-w-- other foo
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 (alias: lss) shows all stores with key counts and file sizes:
❯ pda list-stores
Keys Size Store
2 1.8k @birthdays
12 4.2k @store
--short prints only the names:
❯ pda list-stores --short
@birthdays
@store
Save to a specific store with the @STORE syntax:
pda set alice@birthdays "11/11/1998"
List a specific store:
❯ pda ls @birthdays
Store Key Value
birthdays alice 11/11/1998
birthdays bob 05/12/1980
pda move-store (alias: mvs) renames a store:
pda move-store birthdays bdays
Copy a store with --copy:
pda move-store birthdays bdays --copy
--safe skips if the destination already exists:
pda move-store birthdays bdays --safe
pda remove-store (alias: rms) deletes a store:
pda remove-store birthdays
--yes / -y skips confirmation prompts:
pda remove-store birthdays -y
Import & Export
↑ ·
pda help export,
pda help import
pda export exports everything as NDJSON (it's an alias for list --format ndjson):
pda export > my_backup
Filter exports with --key, --value, and --store:
# export only matching keys
pda export --key "a*"
# export only entries whose values contain a URL
pda export --value "**https**"
pda import 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.
# 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:
pda import mystore -f my_backup
# ok restored 5 entries into @mystore
Read from stdin:
pda import < my_backup
Filter imports with --key and --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
--drop does a full replace — drops all existing entries before importing:
pda import --drop -f my_backup
--interactive / -i prompts before overwriting existing keys.
export encodes binary data as base64. Encryption, read-only, pinned flags, and TTL are all preserved through export and import.
Templates
Values support Go's text/template syntax. Templates are evaluated on pda get and pda run.
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
Template variables are substituted from KEY=VALUE arguments passed to pda get:
pda set greeting "Hello, {{ .NAME }}"
❯ pda get greeting NAME="Alice"
Hello, Alice
default
default sets a fallback value when a variable is missing or empty:
pda set greeting "Hello, {{ default "World" .NAME }}"
❯ pda get greeting
Hello, World
❯ pda get greeting NAME="Bob"
Hello, Bob
require
require errors if the variable is missing or empty:
pda set file "{{ require .FILE }}"
❯ pda get file
FAIL cannot get 'file': ...required value is missing or empty
env
env reads from environment variables:
pda set my_name "{{ env "USER" }}"
❯ pda get my_name
llywelwyn
time
time returns the current UTC time in RFC3339 format:
pda set note "Created at {{ time }}"
❯ pda get note
Created at 2025-01-15T12:00:00Z
enum
enum restricts a variable to a set of acceptable values:
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
int parses a variable as an integer, useful for loops and arithmetic:
pda set number "{{ int .N }}"
❯ pda get number N=3
3
Use it in a range loop:
pda set meows "{{ range int .COUNT }}meow! {{ end }}"
❯ pda get meows COUNT=4
meow! meow! meow! meow!
list
list parses a comma-separated string into a list for iteration:
pda set names "{{ range list .NAMES }}Hi {{.}}. {{ end }}"
❯ pda get names NAMES=Bob,Alice
Hi Bob. Hi Alice.
shell
shell executes a command and returns its stdout:
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 (Recursive)
pda gets another key's value, enabling recursive composition:
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
no-template
Pass --no-template to pda get to output the raw value without evaluating templates:
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
↑ ·
pda list,
pda remove,
pda export,
pda import
--key / -k, --value / -v, and --store / -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, export, import, and remove. --value is not available on import or remove.
gobwas/glob is used for matching. The default separators are /-_.@: and space.
Glob Patterns
* wildcards a word or series of characters, stopping at separator boundaries:
❯ 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:
pda ls --key "foo**"
# foo.bar.baz
pda ls --key "**g"
# dog, cog, mouse hotdog
? matches a single character:
pda ls --key "?og"
# dog, cog
[abc] matches one of the characters in the brackets:
pda ls --key "[dc]og"
# dog, cog
# negate with '!'
pda ls --key "[!dc]og"
# bog (if it exists)
[a-c] matches a range:
pda ls --key "[a-g]ag"
# bag, gag
pda ls --key "[!a-g]ag"
# wag
Filtering by Key
--key / -k filters entries by key name:
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
--value / -v filters by value content using the same glob syntax:
❯ pda ls --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
Locked (encrypted without an available identity) and non-UTF-8 (binary) entries are silently excluded from --value matching.
Filtering by Store
--store / -s filters by store name:
pda ls --store "prod*"
pda export --store "dev*"
Combining Filters
Combine key, value, and store filters. Results must match all flags (AND), with OR within each flag:
pda ls --key "db*" --value "**localhost**"
Globs can be arbitrarily complex, and --key can be combined with exact positional args on rm:
pda rm cat --key "{mouse,[cd]og}**"
# ??? remove 'cat'? (y/n)
# ==> y
# ??? remove 'mouse trap'? (y/n)
# ...
Binary Data
↑ ·
pda set,
pda get,
pda list
pda! supports all binary data. Save it with pda set:
pda set logo < logo.png
pda set logo -f logo.png
And retrieve it with pda get:
pda get logo > output.png
On a TTY, get and list show a summary for binary data. If piped or run outside of a TTY, raw bytes are output:
❯ pda get logo
(binary: 4.2 KB, image/png)
--base64 / -b views binary data as base64:
❯ pda get logo --base64
iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADklEQVQI12...
pda export encodes binary data as base64 in the NDJSON:
❯ pda export
{"key":"logo","value":"89504E470D0A1A0A0000000D4948445200000001000000010802000000","encoding":"base64"}
pda edit presents binary values as base64 for editing and decodes them back on save.
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
pda init initialises version control:
# initialise an empty repository
pda init
# or clone an existing one
pda init https://github.com/llywelwyn/my-repository
--clean removes the existing .git directory first, useful for reinitialising or switching remotes:
pda init --clean
pda init https://github.com/llywelwyn/my-repository --clean
Sync
pda 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.
# sync with Git
pda sync
# with a custom commit message
pda sync -m "added production credentials"
Running pda sync manually will always fetch, commit, and push — or stash and pull if behind — regardless of config.
Auto-Commit & Auto-Push
pda! supports automation via its 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 manually when switching machines.
Identity
pda identity (alias: id) manages the age encryption identity used for encryption.
Viewing Identity
With no flags, pda identity shows your public key, identity file path, and any additional recipients:
❯ pda identity
ok pubkey age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p
ok identity ~/.config/pda/identity.txt
--path prints only the identity file path:
❯ pda identity --path
~/.config/pda/identity.txt
Creating an Identity
An identity is generated automatically the first time you use --encrypt. To create one manually:
pda identity --new
--new errors if an identity already exists. Delete the file manually to replace it.
Recipients
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:
❯ pda identity --add-recipient age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p
ok re-encrypted api-key
ok added recipient age1ql3z...
ok re-encrypted 1 secret(s)
Remove a recipient with --remove-recipient. Secrets are re-encrypted without their key:
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...
Config
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 manages configuration without editing files by hand:
# list all config values and their current settings
pda config list
# get a single value
❯ pda config get git.auto_commit
false
# set a value (validated before saving)
pda config set git.auto_commit true
# open in $EDITOR (validated on save)
pda config edit
# print the config file path
pda config path
# generate a fresh default config file
pda config init
# overwrite an existing config with defaults
pda config init --new
# update config: migrate deprecated keys and fill missing defaults
pda config init --update
pda doctor will warn about unrecognised keys (typos, removed options) and show any non-default values, so it doubles as a config audit.
Example config.toml
All values below are the defaults. A missing config file or missing keys will use these values.
# display ascii header in long root and version commands
display_ascii_art = true
[key]
# prompt y/n before deleting keys
always_prompt_delete = false
# prompt y/n before deleting with a glob match
always_prompt_glob_delete = true
# prompt y/n before key overwrites
always_prompt_overwrite = false
# encrypt all values at rest by default
always_encrypt = false
[store]
# store name used when none is specified
default_store_name = "store"
# prompt y/n before deleting whole store
always_prompt_delete = true
# prompt y/n before store overwrites
always_prompt_overwrite = true
[list]
# list all, or list only the default store when none specified
always_show_all_stores = true
# default output, accepts: table|tsv|csv|markdown|html|ndjson|json
default_list_format = "table"
# show full values without truncation
always_show_full_values = false
# suppress the header row
always_hide_header = false
# columns and order, accepts: meta,size,ttl,store,key,value
default_columns = "meta,size,ttl,store,key,value"
[git]
# auto fetch whenever a change happens
auto_fetch = false
# auto commit any changes
auto_commit = false
# auto push after committing
auto_push = false
# commit message if none manually specified
# supports templates, see: #templates section
default_commit_message = "{{ summary }} {{ time }}"
Environment
↑ ·
Config,
pda doctor
PDA_CONFIG overrides the config directory. pda! will look for config.toml in this directory:
PDA_CONFIG=/tmp/config/ pda set key value
PDA_DATA overrides the data storage directory:
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 and pda config edit to open values in a text editor. Must be set for these commands to work:
EDITOR=nvim pda edit mykey
SHELL is used by pda run (or pda get --run) for command execution. Falls back to /bin/sh if unset:
pda run script
Doctor
↑ · Config, Environment
pda doctor runs a set of health checks of your environment:
❯ 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.
Version
# 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.
Help
set ·
get ·
run ·
list ·
edit ·
move ·
copy ·
remove ·
meta ·
identity ·
export ·
import ·
list-stores ·
move-store ·
remove-store ·
init ·
sync ·
git ·
config ·
doctor ·
version
pda set
Set a key to a given value or stdin. Optionally specify a store.
Pass --encrypt to encrypt the value at rest using age. An identity file
is generated automatically on first use.
PDA supports parsing Go templates. Actions are delimited with {{ }}.
For example:
'Hello, {{ .NAME }}' can be substituted with NAME="John Doe".
'Hello, {{ env "USER" }}' will fetch the USER env variable.
'Hello, {{ default "World" .NAME }}' will default to World if NAME is blank.
'Hello, {{ require .NAME }}' will error if NAME is blank.
'{{ enum .NAME "Alice" "Bob" }}' allows only NAME=Alice or NAME=Bob.
Usage:
pda set KEY[@STORE] [VALUE] [flags]
Aliases:
set, s
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 get
↑ · See also: Getting, Templates
Get the value of a key. Optionally specify a store.
{{ .TEMPLATES }} can be filled by passing TEMPLATE=VALUE as an
additional argument after the initial KEY being fetched.
For example:
pda set greeting 'Hello, {{ .NAME }}!'
pda get greeting NAME=World
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 run
↑ · See also: Running, Templates
Get the value of a key and execute it as a shell command. Optionally specify a store.
{{ .TEMPLATES }} can be filled by passing TEMPLATE=VALUE as an
additional argument after the initial KEY being fetched.
For example:
pda set greeting 'Hello, {{ .NAME }}!'
pda run greeting NAME=World
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
pda list
↑ · See also: Listing, Filtering
List the contents of all stores.
By default, list shows entries from every store. Pass a store name as a
positional argument to narrow to a single store, or use --store/-s with a
glob pattern to filter by store name.
Use --key/-k and --value/-v to filter by key or value glob, and --store/-s
to filter by store name. All filters are repeatable and OR'd within the
same flag.
Usage:
pda list [STORE] [flags]
Aliases:
list, ls
Flags:
-a, --all list across all stores
-b, --base64 view binary data as base64
-c, --count print only the count of matching entries
-o, --format format output format (table|tsv|csv|markdown|html|ndjson|json)
-f, --full show full values without truncation
-h, --help help for list
-k, --key strings filter keys with glob pattern (repeatable)
--no-header suppress the header row
--no-keys suppress the key column
--no-meta suppress the meta column
--no-size suppress the size column
--no-store suppress the store column
--no-ttl suppress the TTL column
--no-values suppress the value column
-s, --store strings filter stores with glob pattern (repeatable)
-v, --value strings filter values with glob pattern (repeatable)
pda edit
Open a key's value in $EDITOR. If the key doesn't exist, opens an
empty file — saving non-empty content creates the key.
Binary values are presented as base64 for editing and decoded back on save.
Metadata flags (--ttl, --encrypt, --decrypt) can be passed alongside the edit
to modify metadata in the same operation.
Usage:
pda edit KEY[@STORE] [flags]
Aliases:
edit, e
Flags:
-d, --decrypt decrypt the value (store as plaintext)
-e, --encrypt encrypt the value at rest
--force bypass read-only protection
-h, --help help for edit
--pin pin the key (sorts to top in list)
--preserve-newline keep trailing newlines added by the editor
--readonly mark the key as read-only
--ttl string set expiry (e.g. 30m, 2h) or 'never' to clear
--unpin unpin the key
--writable clear the read-only flag
pda move
↑ · See also: Moving & Copying
Move a key
Usage:
pda move FROM[@STORE] TO[@STORE] [flags]
Aliases:
move, mv
Flags:
--copy copy instead of move (keeps source)
--force bypass read-only protection
-h, --help help for move
-i, --interactive prompt before overwriting destination
--safe do not overwrite if the destination already exists
-y, --yes skip all confirmation prompts
pda copy
↑ · See also: Moving & Copying
Make a copy of a key
Usage:
pda copy FROM[@STORE] TO[@STORE] [flags]
Aliases:
copy, cp
Flags:
--force bypass read-only protection
-h, --help help for copy
-i, --interactive prompt before overwriting destination
--safe do not overwrite if the destination already exists
-y, --yes skip all confirmation prompts
pda remove
↑ · See also: Removing, Filtering
Delete one or more keys
Usage:
pda remove KEY[@STORE] [KEY[@STORE] ...] [flags]
Aliases:
remove, rm
Flags:
--force bypass read-only protection
-h, --help help for remove
-i, --interactive prompt yes/no for each deletion
-k, --key strings delete keys matching glob pattern (repeatable)
-s, --store strings target stores matching glob pattern (repeatable)
-v, --value strings delete entries matching value glob pattern (repeatable)
-y, --yes skip all confirmation prompts
pda meta
↑ · See also: Metadata, TTL, Encryption, Read-Only, Pinned
View or modify metadata (TTL, encryption, read-only, pinned) for a key
without changing its value.
With no flags, displays the key's current metadata. Pass flags to modify.
Usage:
pda meta KEY[@STORE] [flags]
Flags:
-d, --decrypt decrypt the value (store as plaintext)
-e, --encrypt encrypt the value at rest
--force bypass read-only protection for metadata changes
-h, --help help for meta
--pin pin the key (sorts to top in list)
--readonly mark the key as read-only
--ttl string set expiry (e.g. 30m, 2h) or 'never' to clear
--unpin unpin the key
--writable clear the read-only flag
pda identity
↑ · See also: Identity, Encryption
Show or create the age encryption identity
Usage:
pda identity [flags]
Aliases:
identity, id
Flags:
--add-recipient string add an age public key as an additional encryption recipient
-h, --help help for identity
--new generate a new identity (errors if one already exists)
--path print only the identity file path
--remove-recipient string remove an age public key from the recipient list
pda export
↑ · See also: Import & Export
Export store as NDJSON (alias for list --format ndjson)
Usage:
pda export [STORE] [flags]
Flags:
-h, --help help for export
-k, --key strings filter keys with glob pattern (repeatable)
-s, --store strings filter stores with glob pattern (repeatable)
-v, --value strings filter values with glob pattern (repeatable)
pda import
↑ · See also: Import & Export
Restore key/value pairs from an NDJSON dump
Usage:
pda import [STORE] [flags]
Flags:
--drop drop existing entries before restoring (full replace)
-f, --file string path to an NDJSON dump (defaults to stdin)
-h, --help help for import
-i, --interactive prompt before overwriting existing keys
-k, --key strings restore keys matching glob pattern (repeatable)
-s, --store strings restore entries from stores matching glob pattern (repeatable)
pda list-stores
List all stores
Usage:
pda list-stores [flags]
Aliases:
list-stores, lss
Flags:
-h, --help help for list-stores
--no-header suppress the header row
--short only print store names
pda move-store
Rename a store
Usage:
pda move-store FROM TO [flags]
Aliases:
move-store, mvs
Flags:
--copy copy instead of move (keeps source)
-h, --help help for move-store
-i, --interactive prompt before overwriting destination
--safe do not overwrite if the destination store already exists
-y, --yes skip all confirmation prompts
pda remove-store
Delete a store
Usage:
pda remove-store STORE [flags]
Aliases:
remove-store, rms
Flags:
-h, --help help for remove-store
-i, --interactive prompt yes/no for each deletion
-y, --yes skip all confirmation prompts
pda init
Initialise pda! version control
Usage:
pda init [remote-url] [flags]
Flags:
--clean remove .git from stores directory before initialising
-h, --help help for init
pda sync
Manually sync your stores with Git
Usage:
pda sync [flags]
Flags:
-h, --help help for sync
-m, --message string custom commit message (defaults to timestamp)
pda git
Run any arbitrary command. Use with caution.
The Git repository lives directly in the data directory
("PDA_DATA"). Store files (*.ndjson) are tracked by Git as-is.
If you manually modify files without using the built-in
commands, you may desync your repository.
Generally prefer "pda sync".
Usage:
pda git [args...] [flags]
Flags:
-h, --help help for git
pda config
View and modify configuration
Usage:
pda config [command]
Available Commands:
edit Open config file in $EDITOR
get Print a configuration value
init Generate default config file
list List all configuration values
path Print config file path
set Set a configuration value
Flags:
-h, --help help for config
Use "pda config [command] --help" for more information about a command.
pda doctor
Check environment health
Usage:
pda doctor [flags]
Flags:
-h, --help help for doctor
pda version
Display pda! version
Usage:
pda version [flags]
Flags:
-h, --help help for version
--short print only the version string
License
MIT — see LICENSE.