personal digital assistant! A key/value store tool for the command line with rich template support, written in Go.
Find a file
2026-03-16 16:31:37 +00:00
.github/workflows bump Go version in CI 2025-11-20 18:35:42 +00:00
cmd feat(completions): wire up store completions and --store flag completions 2026-03-16 16:31:37 +00:00
docs feat(docs): remove cmds.png 2025-11-20 19:15:45 +00:00
testdata feat: adds --readonly and --pin flags, and displays Size column in list by default 2026-02-13 18:52:34 +00:00
vhs feat(Envs): unifies overrides to PDA_DATA and PDA_CONFIG, and sets them to /tmp/ for tests. 2025-12-18 21:50:01 +00:00
.gitignore chore: add .worktrees to gitignore 2026-02-11 00:31:21 +00:00
go.mod feat: encryption with age 2026-02-11 12:36:42 +00:00
go.sum feat: encryption with age 2026-02-11 12:36:42 +00:00
LICENSE feat(init): initial commit 2025-11-06 15:00:18 +00:00
main.go fix(DB): validates db name for path traversal 2025-12-18 15:04:32 +00:00
main_test.go feat(edit): add edit command to open key values in $EDITOR 2026-02-13 15:27:16 +00:00
README.md docs: added redlinks 2026-03-16 12:45:27 +00:00

pda

pda! is a command-line key-value store tool with:

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.

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

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

· Templates · pda get

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 · Filtering

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

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

· pda move, pda copy

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

pda remove (alias: rm) deletes one or more keys. Any number of keys can be deleted in a single call, with keys from differing stores able to be mixed freely.

# delete a single key
pda remove kitty

# delete multiple keys at once
pda remove kitty doggy

# delete across stores
pda remove kitty secret@private

Exact positional keys can be combined with glob patterns via key, store, and value to widen the scope of a deletion. Glob-matched deletions prompt for confirmation by default due to their more error-prone nature. This is configurable with key.always_prompt_glob_delete in the config.

# delete "kitty" and everything matching the key "?og"
 pda rm kitty --key "?og"
 ??? remove 'cog'? (y/n)
 ==> y
 ??? remove 'dog'? (y/n)

# delete keys matching a store and key pattern
 pda rm --store "temp*" --key "session*"

Passing interactive prompts before each deletion, including exact keys. This behaviour can be made permanent with key.always_prompt_delete in the config. Inversely, yes auto-accepts all confirmation prompts.

# prompt before each deletion
 pda rm kitty -i
 ??? remove 'kitty'? (y/n)
 ==> y

# auto-accept all prompts
 pda rm kitty -y

Read-only keys cannot be deleted without explicitly passing force.

# remove a read-only key
 pda rm protected-key
FAIL cannot remove 'protected-key': key is read-only

# force-remove a read-only key
 pda rm protected-key --force

Metadata

· pda meta, TTL, Encryption, Read-Only, Pinned

pda meta can be used to view or modify metadata for a given key without touching its value. It always takes one argument: the desired key.

If no flags are passed, pda meta will display the key's current metadata. Any flags passed can be used to modify metadata in-place: ttl, encrypt or decrypt, readonly or writable, and pin or unpin. Multiple changes can be combined in a single command.

In pda list output, metadata is demonstrated via a Meta column. The presence of each type of metadata is marked by a character, or a dash if unset: (e)ncrypted, (w)ritable, (t)ime-to-live, and (p)inned.

# view a key's underlying metadata
 pda meta session
  key: session@store
  secret: false
  writable: true
  pinned: false
  expires: 59m30s

# make a key read-only
 pda meta session --readonly

# remove a key's expiration time
 pda meta session --ttl never

Modifying a read-only key's metadata requires force or by first making it writable. A read-only key can still be pinned or unpinned as pin state only determines where a key is on list output[#listing], and does not change the actual key state.

TTL

· pda set, pda meta

Keys can be given an expiration time. Expired keys are marked for garbage collection and deleted on the next access to the store. TTL can be set at creation time via pda set --ttl, or toggled later with pda meta --ttl and pda edit --ttl.

# expire after 1 hour
pda set session "123" --ttl 1h

# expire after 54 minutes and 10 seconds
pda set session2 "xyz" --ttl 54m10s

# remove an expiration time
pda meta session --ttl never

TTL can be displayed with pda list in the TTL column, or with pda meta.

# view ttl in a store's list output
 pda ls
   TTL Key      Value
59m30s session  123
51m40s session2 xyz

# view the metadata of a specific key
 pda meta session

Expiration time is preserved on import and export and moving or copying. TTL is stored as a timestamp rather than a timer; keys with a TTL are checked on access to the store they reside in, and any with an expiry that has already passed are deleted. If a key expires while having been exported, it will be deleted on import the next time pda touches the file.

Encryption

· pda set, pda meta, pda identity

pda set --encrypt encrypts values at rest using age. Values are stored on disk as age ciphertext and decrypted automatically at run-time by commands like pda get and pda list when the correct identity file is present. An X25519 identity is generated on first use.

By default, the only recipient for encrypted keys is your own identity file. Additional recipients can be added or removed via pda identity.

# create a key called "api-key" and encrypt it
 pda set --encrypt api-key "sk-live-abc123"
 ok created identity at ~/.local/share/pda/identity.txt

# encrypt a key after editing in $EDITOR
 pda edit --encrypt api-key

# decrypt a key via meta
 pda meta --decrypt api-key

Because the on-disk value of an encrypted key is ciphertext, 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 by setting a new value without --encrypt will warn you.

# setting a new key and forgetting the "--encrypt" flag
 pda set api-key "oops"
WARN overwriting encrypted key 'api-key' as plaintext
hint pass --encrypt to keep it encrypted

If your identity file does not match an intended recipient of an encrypted key, the value will be inaccessible. It will display locked on fetch until a matching identity is found.

 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)

Encrypted keys can be made the default by enabling key.always_encrypt in the config.

Read-Only

· pda set, pda meta

Keys marked read-only are protected from accidental modification. A read-only flag can be set at creation time, toggled later with pda meta, or applied alongside an edit. Making a key writable again or explicitly passing force allows changes through. A key being made writable is a permanent change, whereas the force flag is a one-off.

# create a read-only key
pda set api-url "https://prod.example.com" --readonly

# set a key to read-only with meta
 pda meta api-url --readonly
  ok made readonly api-url

# set a key as writable with meta
 pda meta api-url --writable
  ok made writable api-url

# edit a key, and set as readonly on save
 pda edit notes --readonly

Read-only keys are protected from setting, removing, moving, and editing. They are not protected from the deletion of an entire store.

# set a new value to a read-only key
 pda set api-url "new value"
FAIL cannot set 'api-url': key is read-only

# force changes to a read-only key with the force flag
 pda set api-url "new value" --force
 pda remove api-url --force
 pda move api-url new-name --force

pda copy 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 set, pda meta

Pinned keys sort to the top of pda list output, preserving alphabetical order within the pinned and unpinned groups. A pin can be set at creation time, toggled with pda meta, or applied alongside an edit.

# pin a key at creation time
pda set important "remember this" --pin

# pin a key with meta
 pda meta todo --pin
  ok pinned todo

# unpin a key with meta
 pda meta todo --unpin
  ok unpinned todo

# view pinned keys in list output, at the top
 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

Stores are saved on disk as NDJSON files. pda supports any number of stores, and creating them is automatic. If a key is created with a @STORE suffix, and the named store does not already exist, it will be created automatically to support the new key.

pda list-stores (alias: lss) shows all stores with their respective key counts and file sizes. Passing short prints only the store names.

# list all stores
 pda list-stores
Keys  Size Store
   2 1.8k @birthdays
  12 4.2k @store

# list all store names
 pda list-stores --short
@birthdays
@store

pda move-store (alias: mvs) renames a store. Passing copy keeps the source intact.

# rename a store
pda move-store birthdays bdays

# copy a store
pda move-store birthdays bdays --copy

pda remove-store (alias: rms) deletes a store. A confirmation prompt is shown by default (configurable with store.always_prompt_delete in the config). Deleting an entire store does not require unsetting read-only on contained keys: if a read-only key is within a store, it will be deleted if the store is removed.

# delete a store
pda remove-store birthdays

As with changeful key operations, store commands support interactive and safe flags where they make sense. Moving or removing a store interactively will generate a confirmation prompt if anything would be lost by the action being taken. The safe flag will prevent moving a store from ever overwriting another store.

Inversely, yes can be passed to bypass any confirmation prompts.

Import & Export

· pda export, pda import

pda export dumps entries as NDJSON (it is functionally an alias for list --format ndjson). The filtering flags key, value, and store all work with exports. It shares functionality with pda list in regard to which stores get exported: if list.always_show_all_stores is set and no store name is specified as an argument, all stores will be exported.

# export everything
pda export > my_backup

# 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. The default behaviour for an import is to merge with any existing stores of the same name. To completely replace existing stores instead of merging, drop can be passed.

Importing takes one or zero arguments. On export each key saves the name of the store it came from in its metadata; on import, by default, each key will be returned to that same store. If a store name is passed as an argument to pda import, this behaviour will be overriden and all keys will be imported into the specified store.

As with exporting, key, value, and store flags can be passed to filter which keys will be imported from the input file.

# entries are routed to their original stores
pda import -f my_backup
#   ok restored 5 entries

# force all entries into a single store
pda import mystore -f my_backup
#   ok restored 5 entries into @mystore

# read from stdin
pda import < my_backup

interactive can be passed to pda import to prompt on potential overwrite, and is generally recommended if an import is ever being routed to a specific store, as it is likely to cause collisions.

pda export encodes binary data as base64. All metadata is preserved through export and import.

Templates

· pda get, pda run

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 better accomodate text/template, 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, in addition to summary, which returns the action that triggered the commit (e.g. "set foo", "removed bar").

no-template can be passed to output a raw value without resolving the template.

Basic Substitution

· Templates

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

· Templates

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

· Templates

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

· Templates

env reads from environment variables:

pda set my_name "{{ env "USER" }}"

 pda get my_name
llywelwyn

time

· Templates

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

· Templates

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

· Templates

int parses a variable as an integer. Useful mostly for loops or arithmetic.

 pda set number "{{ int .N }}"
 pda get number N=3
3

# using "int" in a loop
 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:

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. Commands executed by the shell function are executed by $SHELL. If it is somehow unset, it defaults to using /usr/sh.

 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

· Templates

pda returns the output of pda get on a key.

# use the value of "base_url" in another key
 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

# use the value of a key from another store
 pda set host@urls "https://example.com"
 pda set api '{{ pda "host@urls" }}/api'
 pda get api
https://example.com/api

Filtering

· pda list, pda remove, pda export, pda import

key, value, and store flags can be used to filter entries via globs. 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 pda list, pda export, pda import, and pda remove.

gobwas/glob is used for matching. The default separators are /-_.@: and space. For a detailed guide to globbing, I highly recommend taking a look at the documentation there directly.

Glob Patterns

· Filtering

* wildcards a word or series of characters, stopping at separator boundaries.

# list all store contents
 pda ls
cat
mouse house
foo.bar.baz

# match any single-word key
 pda ls --key "*"
cat

# match any two-word key
 pda ls --key "* *"
mouse house

# match any key starting with "foo." and ending with ".baz", with one word between
 pda ls --key "foo.*.baz"
foo.bar.baz

** super-wildcards ignore word boundaries.

# match anything beginning with "foo"
 pda ls --key "foo**"
foo.bar.baz

? matches a single character:

# match anything beginning with any letter, and ending with "og"
 pda ls --key "?og"
dog
cog

[abc] matches one of the characters in the brackets.

# match anything beginning with "d" or "c", and ending with "og"
 pda ls --key "[dc]og"
dog
cog

# negate with '!'
 pda ls --key "[!dc]og"
bog

[a-c] matches a range:

# match anything beginning with "a" to "g", and ending with "ag"
 pda ls --key "[a-g]ag"
bag
gag

# negate with '!'
 pda ls --key "[!a-g]ag"
wag

Filtering by Key

· Filtering, pda list

key filters entries by key name. Multiple key patterns are OR'd. An entry matches if it matches any of them.

pda ls --key "db*"
pda ls --key "session*" --key "token*"

Filtering by Value

· Filtering, pda list

value filters by value. Multiple value patterns are OR'd.

 pda ls --value "**localhost**"
 pda ls --value "**world**" --value "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 filters by store name. Multiple store patterns are OR'd.

pda ls --store "prod*"
pda export --store "dev*"

Combining Filters

· Filtering

key, value, and store filters can be combined. Results must match at least one of each category of filter used. For example, checking for key and two different value globs on the same filter: the results must match key and at least one of the two value globs; the results do not need to match both values.

pda ls --key "db*" --value "**localhost**"

Globs can be combined to create some deeply complex queries. For example, key can be combined with exact positional args on rm to remove exactly the "cat" key, and any keys beginning with "cat", "dog", or "mouse" followed by zero-or-more additional words.

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. Values can be read from a file with --file or piped in via stdin. Retrieval works the same way — pipe or redirect the output to get the raw bytes.

# store binary data from a file
pda set logo < logo.png
pda set logo -f logo.png

# retrieve binary data
pda get logo > output.png

On a TTY, get and list show a summary instead of printing raw bytes (which can cause undefined terminal behaviour). In a non-TTY setting (piped or redirected), the raw bytes are returned as expected. Passing base64 provides a safe way to view binary data in a terminal.

# TTY shows a summary
 pda get logo
(binary: 4.2 KB, image/png)

# base64 view
 pda get logo --base64
iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADklEQVQI12...

pda export encodes binary data as base64 in the NDJSON, and pda edit presents binary values as base64 for editing and decodes them back on save.

 pda export
{"key":"logo","value":"89504E470D0A1A0A0000000D4948445200000001000000010802000000","encoding":"base64"}

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

pda init initialises version control. With no arguments it creates a local-only repository in the data directory. Pass a remote URL to clone from an existing repository instead.

# initialise an empty local repository
pda init

# or clone from an existing remote
pda init https://github.com/llywelwyn/my-repository

Passing clean removes any 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

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 config

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, Encryption

pda identity (alias: id) manages the age encryption identity used for encryption.

Viewing Identity

· pda identity

With no flags, pda identity shows your public key, identity file path, and any additional recipients. Passing path prints only the identity file path, useful for scripting.

 pda identity
  ok pubkey  age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p
  ok identity  ~/.config/pda/identity.txt

 pda identity --path
~/.config/pda/identity.txt

Creating an Identity

· pda 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

· pda identity

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)

Removing a recipient with --remove-recipient re-encrypts all secrets without their key. Additional recipients are shown in the default identity display.

pda identity --remove-recipient age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p

 pda identity
  ok pubkey     age1abc...
  ok identity   ~/.local/share/pda/identity.txt
  ok recipient  age1ql3z...

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 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

· Config, pda config

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! respects a small set of environment variables for overriding paths and tools. These are primarily useful for isolating stores across environments or for scripting.

PDA_CONFIG overrides the config directory — pda! will look for config.toml here instead of the default XDG location. PDA_DATA overrides the data storage directory where stores and the Git repository live. Default data locations follow XDG conventions: ~/.local/share/pda/ on Linux, ~/Library/Application Support/pda/ on macOS, and %LOCALAPPDATA%/pda/ on Windows.

# use an alternative config directory
PDA_CONFIG=/tmp/config/ pda set key value

# use an alternative data directory
PDA_DATA=/tmp/stores pda set key value

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. SHELL is used by pda run (or pda get --run) for command execution, falling back to /bin/sh if unset.

EDITOR=nvim pda edit mykey

Doctor

· Config, Environment

pda doctor runs a set of health checks against your environment, covering installed tools, config validity, store integrity, and Git status.

 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

· pda version

pda version displays the current version. Passing short prints just the release string without ASCII art, useful for scripting. pda! uses calendar versioning: YYYY.WW. ASCII art can be permanently disabled with display_ascii_art = false in config.

# display the full version output
pda version

# or just the release
 pda version --short
pda! 2025.52 Christmas release

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

· See also: Setting

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

· See also: Editing

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

· See also: 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

· See also: Stores

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

· See also: Stores

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

· See also: Init, Git

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

· See also: Sync, Git

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

· See also: 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

· See also: 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

· See also: Doctor

Check environment health

Usage:
  pda doctor [flags]

Flags:
  -h, --help   help for doctor

pda version

· See also: Version

Display pda! version

Usage:
  pda version [flags]

Flags:
  -h, --help    help for version
      --short   print only the version string

License

MIT — see LICENSE.