personal digital assistant! A key/value store tool for the command line with rich template support, written in Go.
Find a file
2026-02-14 01:41:53 +00:00
.github/workflows bump Go version in CI 2025-11-20 18:35:42 +00:00
cmd fix(get): prevents templating invalid utf8 values 2026-02-14 01:41:40 +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 chore(docs update): 2026-02-14 01:41:53 +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. Every store is plaintext, portable, and yours. There's no daemon, no cloud service, and no proprietary format. Keys are just lines in a JSON file; stores are just files in a directory. If you can cat a file, you can read your data without pda! installed.

Git versioning is built in. Enable auto-committing, pushing, and fetching in the config to automatically version every change, or just run pda sync when you want to. Because the storage format is line-oriented plaintext, diffs are meaningful and merges are clean.

Go's text/template engine is available on every value at retrieval time, turning simple key-value pairs into dynamic snippets with variables, environment lookups, shell execution, cross-references, and more.

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

pda! MIT licensed. (c) 2025 Lewis Wynne

Usage:
  pda [command]

Key commands:
  copy         Make a copy of a key
  edit         Edit a key's value in $EDITOR
  get          Get the value of a key
  identity     Show or create the age encryption identity
  list         List the contents of all stores
  meta         View or modify metadata for a key
  move         Move a key
  remove       Delete one or more keys
  run          Get the value of a key and execute it
  set          Set a key to a given value

Store commands:
  export       Export store as NDJSON (alias for list --format ndjson)
  import       Restore key/value pairs from an NDJSON dump
  list-stores  List all stores
  move-store   Rename a store
  remove-store Delete a store

Git commands:
  git          Run any arbitrary command. Use with caution.
  init         Initialise pda! version control
  sync         Manually sync your stores with Git

Environment commands:
  config       View and modify configuration
  doctor       Check environment health

Additional Commands:
  completion   Generate the autocompletion script for the specified shell
  help         Help about any command
  version      Display pda! version

Most commands have aliases and flags. Run pda help [command] to see them.

Key commands

· pda set, pda get, pda run, pda list, pda edit, pda move, pda remove

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. Values can come from arguments, stdin, or a file.

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 set requires a key and a value as inputs. The first argument given will always be used to determine the key.

# create a key-value pair
pda set name "Alice"

# 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 a key-value pair in the "Favourites" store
pda set movie@favourites "The Road"

# create an encrypted key-value pair, expiring in one day
pda set secret "Secret data." --encrypt --ttl 24h

The interactive and safe flags exist to prevent accidentally overwriting an existing key when creating a new one. These flags exist on all writable commands.

# 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) retrieves a key's value. Templates are evaluated at retrieval time.

Usage:
  pda get KEY[@STORE] [flags]

Aliases:
  get, g

Flags:
  -b, --base64        view binary data as base64
      --exists        exit 0 if the key exists, exit 1 if not (no output)
  -h, --help          help for get
      --no-template   directly output template syntax
  -c, --run           execute the result as a shell command

[pda get] takes one argument: the desired key. The value is output to stdout.

# 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
{{ env "USER" }}

An alternative to templates is the run flag. For detailed information, see pda run, an alias for pda get --run.

# create a key containg a script
 pda set my_script "echo Hello, world."

# get and run a key using $SHELL
 pda get my_script --run
Hello, world.

Running

pda run retrieves a key and executes it as a shell command. It uses the shell set in $SHELL. If, somehow, this environment variable is unset, it falls back and attempts to use /bin/sh. Templates are functional when running a key directly.

Usage:
  pda run KEY[@STORE] [flags]

Flags:
  -b, --base64        view binary data as base64
  -h, --help          help for run
      --no-template   directly output template syntax

Running takes one argument: the key.

# 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) shows what you've got stored. The default columns are meta,size,ttl,store,key,value. Meta is a 4-char flag string: (e)ncrypted (w)ritable (t)tl (p)inned, or a dash for an unset flag.

 pda ls
Meta Size TTL Store Key  Value
-w-p    5   - store todo don't forget this
----   23   - store url  https://prod.example.com
-w--    5   - store name Alice

By default, pda list shows entries from every store. Pass a store name to narrow to a single store:

pda ls @store

Use --store / -s to filter stores by glob pattern:

pda ls --store "prod*"

Filter by key or value with --key / -k and --value / -v:

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

Columns can be toggled with --no-X flags. --no-X suppresses a column; --no-X=false adds it even if it's not in the default config:

# hide the meta and size columns
pda ls --no-meta --no-size

Long values are truncated to fit the terminal. --full / -f shows the complete value:

 pda ls
Key  Value
note this is a very long (..30 more chars)

 pda ls --full
Key  Value
note this is a very long value that keeps on going and going

--count / -c prints only the count of matching entries:

 pda ls --count
3

 pda ls --count --key "d*"
1

--format / -o selects the output format. Available formats: table (default), csv, tsv, json, ndjson, markdown, html:

 pda ls --format csv
Meta,Size,TTL,Store,Key,Value
-w--,5,-,store,name,Alice

 pda ls --format json
[{"key":"name","value":"Alice","encoding":"text","store":"store"}]

--all / -a lists across all stores (default when list.always_show_all_stores is true).

--base64 / -b shows binary data as base64.

--no-header suppresses the header row.

Pinned entries sort to the top, preserving alphabetical order within the pinned and unpinned groups.

See also: pda help list

Editing

· pda edit, pda set, pda meta

pda edit (alias: e) opens a key's value in your $EDITOR. If the key doesn't exist, an empty file is opened — saving non-empty content creates it.

# edit an existing key
pda edit name

# edit a new key — saving non-empty content creates it
pda edit newkey

Metadata flags can be passed alongside the edit to modify metadata in the same operation:

pda edit name --ttl 1h --encrypt

Trailing newlines added by the editor are stripped by default. --preserve-newline keeps them:

pda edit name --preserve-newline

--encrypt / -e encrypts the value. --decrypt / -d decrypts it. --readonly and --writable toggle protection. --pin and --unpin toggle pinning. --ttl sets or clears expiry (e.g. 30m, 2h, or never).

Binary values are presented as base64 for editing and decoded back on save.

Read-only keys require --force to edit.

See also: pda help edit

Moving & Copying

· pda move, pda copy

pda move (alias: mv) moves a key to a new name or store. All metadata is preserved.

 pda mv name name2
  ok renamed name to name2

pda copy (alias: cp) makes a copy. The source is kept and all metadata is preserved.

pda cp name name2

mv --copy and cp are equivalent:

pda mv name name2 --copy

Move or copy across stores:

pda mv name@store name@archive
pda cp config@dev config@prod

--safe skips if the destination already exists:

pda mv name name2 --safe
# info skipped 'name2': already exists

--yes / -y skips all confirmation prompts:

pda mv name name2 -y

Read-only keys can't be moved or overwritten without --force:

 pda mv readonly-key newname
FAIL cannot move 'readonly-key': key is read-only

pda mv readonly-key newname --force

cp can copy a read-only key freely (since the source isn't modified), and the copy preserves the read-only flag. Overwriting a read-only destination is blocked without --force.

See also: pda help move, pda help copy

Removing

· pda remove, --key

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

See also: pda help remove

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

See also: pda help meta

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.

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.

See also: pda help set, pda help meta

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

See also: pda help set, pda help meta, pda help identity

Read-Only

· pda set, pda meta, pda edit

Keys marked read-only are protected from accidental modification. You can modify a read-only key again by making it --writable 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.

See also: pda help set, pda help meta, pda help edit

Pinned

· pda set, pda meta, pda list

Pinned keys sort to the top of pda list 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

See also: pda help set, pda help meta

Stores

· pda list-stores, pda move-store, pda remove-store

You can have as many stores as you want. Stores are created implicitly when you set a key with a @STORE suffix. Each store is a separate NDJSON file on disk.

pda list-stores (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

See also: pda help list-stores, pda help move-store, pda help remove-store

Import & Export

· pda export, pda 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.

See also: pda help export, pda help 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 fit text/template into a CLI key-value tool, pda! adds a small set of built-in functions on top of the standard library.

These same functions are also available in git.default_commit_message templates, along with summary which returns the action that triggered the commit (e.g. "set foo", "removed bar").

Basic Substitution

· Templates, pda get

Template variables are substituted from KEY=VALUE arguments passed to pda get:

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

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

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)

· Templates

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

· pda get

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

See also: pda help get, pda help set

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

· Filtering

* 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

· Filtering, pda list

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

· Filtering, pda list

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

· Filtering, pda list

--store / -s filters by store name:

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

Combining Filters

· Filtering

Combine key, value, and store filters. Results must match all flags (AND), with OR within each flag:

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)
# ...

See also: pda help list, pda help remove

Binary Data

· pda set, pda get, pda list

pda! supports all binary data. Save it with pda set:

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.

See also: pda help set, pda help get

Git

· pda init, pda sync, Config

pda! supports automatic version control backed by Git, either in a local-only repository or by initialising from a remote.

Init

· Git, pda sync

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

See also: pda help init

Sync

· Git

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.

See also: pda help sync

Auto-Commit & Auto-Push

· Git, 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:

 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

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

By default, secrets are encrypted only for your own identity. To encrypt for additional recipients (e.g. a teammate or another device), use --add-recipient 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...

See also: pda help identity

Config

· pda config, pda doctor

Config is stored at ~/.config/pda/config.toml (Linux/macOS) or %LOCALAPPDATA%/pda/config.toml (Windows). All values have sensible defaults, so a config file is entirely optional.

Config Commands

· pda config

pda config 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.

See also: pda help config

Example config.toml

· Config

All values below are the defaults. A missing config file or missing keys will use these values.

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

See also: pda help doctor

Help & Version

# help for any command
pda help set
pda help list
pda help config

# display the full version output
pda version

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

pda! uses calendar versioning: YYYY.WW. ASCII art can be permanently disabled with display_ascii_art = false in config.

License

MIT — see LICENSE.