A JSON CLI that lets you create and manage you own KeePass (.kdbx) database — entries, groups, and attachments — no human needed!
---
name: keepass-cli
version: 1.0.1
description: A JSON CLI that lets you create and manage you own KeePass (.kdbx) database — entries, groups, and attachments — no human needed!
metadata: {"keepass":{"category":"secrets","protocol":"kdbx","auth":"master-password"}}
---
# KeePass CLI
Use this CLI to manage your own local KeePass (`.kdbx`) database — your
credentials, API keys, and other secrets — without a human operator in the
loop. It wraps [pykeepass](https://github.com/libkeepass/pykeepass) and
prints structured JSON for every command you run.
## Skill Files
| File | Purpose |
|------|---------|
| **skill.md** (this file) | Skill overview and usage |
| **keepass_cli.py** | CLI implementation |
| **pykeepass_docs.html** | Reference docs for the underlying `pykeepass` library |
## .env.example
```
KEEPASS_DATABASE_PATH="placeholder"
KEEPASS_DATABASE_PASSWORD="placeholder"
```
**Set yourself up before your first call:**
```bash
# 1. install dependencies
python -m pip install python-dotenv pykeepass
# 2. create a .env file using the example provided above
```
## What You Can Do With This CLI
- create your own `.kdbx` database
- inspect database metadata (version, encryption algorithm, counts)
- list, search, add, edit, move, and delete entries
- list, add, move, and delete groups
- list, add, and delete file attachments on entries
## Read the JSON Contract Before You Parse Output
Every invocation prints exactly **one** JSON object to stdout. Read that
object, then check the process exit status (`0` success, `2` handled error,
`1` unexpected error) — don't try to parse stderr or partial output.
```json
{"ok": true, "action": "add-entry", "entry": {"...": "..."}}
```
```json
{"ok": false, "error": "No entry matched the supplied selector.", "error_code": "not_found"}
```
Match on `error_code`, not on the human-readable `error` string — it is
stable across versions. The values you'll see:
`invalid_argument`, `invalid_selector`, `not_found`, `ambiguous_selector`,
`auth_failure`, `database_not_found`, `database_exists`, `database_corrupt`,
`missing_env_var`, `root_protected`, `invalid_move`, `file_not_found`,
`shared_resource`, `no_fields_provided`, `internal_error`. When you get
`internal_error`, check the accompanying `type` field for the Python
exception class.
By default, don't expect entry passwords/OTP or timestamps in the response —
they're withheld unless you explicitly ask for them. Pass `--show-secrets` if
you need `password`/`otp`, and `--include-metadata` if you need
`created`/`modified`/`accessed`/`expires`/`expiry_time`.
## Usage
Every command you run reads the database path from `KEEPASS_DATABASE_PATH`;
override it with `--database` if you need a different file. Entry/attachment
selectors (e.g. `--entry-uuid`/`--title`) are mutually exclusive — pick one.
### Create or inspect the database
```bash
python keepass_cli.py create
python keepass_cli.py create --force
python keepass_cli.py database-info
```
### List, search, or show entries
```bash
python keepass_cli.py list-entries --query github
python keepass_cli.py list-entries --group "Services/APIs" --sort-by title
python keepass_cli.py show-entry --title "GitHub" --show-secrets
```
`--sort-by` accepts `uuid` (default), `title`, `group`, or `modified`.
### Add, edit, delete, or move entries
```bash
python keepass_cli.py add-entry --title "GitHub" --username "agent@example.com" \
--url "https://github.com" --group "Services/APIs" --password "s3cure_p455w0rd"
python keepass_cli.py edit-entry --title "GitHub" --username "new-user"
python keepass_cli.py edit-entry --title "GitHub" --password "new_p455w0rd"
python keepass_cli.py delete-entry --title "GitHub"
python keepass_cli.py delete-entry --title "GitHub" --permanent
python keepass_cli.py move-entry --title "GitHub" --destination-group "Archive"
```
`add-entry` requires `--password`; `edit-entry --password` sets a new one.
Both are managed entirely through the CLI — there is no environment variable
for entry passwords, so choose or generate them yourself before calling the
CLI. When you call `edit-entry`, supply at least one field to change or
you'll get `no_fields_provided`. Your deletes land in the Recycle Bin unless
you pass `--permanent`.
### List, add, delete, or move groups
```bash
python keepass_cli.py list-groups --sort-by path
python keepass_cli.py add-group --name "APIs" --parent-group "Services"
python keepass_cli.py delete-group --group "Services/APIs"
python keepass_cli.py move-group --group "APIs" --destination-group "Archive"
```
You can never delete or move the root group. If you try to move a group into
itself or one of its own descendants, expect `invalid_move`.
### Manage attachments
```bash
python keepass_cli.py list-attachments --title "GitHub"
python keepass_cli.py add-attachment --title "GitHub" --file ./token.pem
python keepass_cli.py delete-attachment --title "GitHub" --attachment-id 0
python keepass_cli.py delete-attachment --title "GitHub" --filename token.pem --delete-binary
```
Select attachments by `--attachment-id` (from `list-attachments`) or
`--filename`. If you pass `--delete-binary` and other attachments still
reference that binary, the call refuses to run unless you also pass
`--force`.
## Response Example
```json
{
"ok": true,
"action": "add-entry",
"entry": {
"uuid": "b6f1...c2",
"title": "GitHub",
"username": "agent@example.com",
"url": "https://github.com",
"notes": "",
"group": "Services/APIs",
"attachments": []
}
}
```
## Notes
- `KEEPASS_DATABASE_PATH` is the location of your `.kdbx` file, used by every command you run.
- `KEEPASS_DATABASE_PASSWORD` unlocks your database.
- Both are loaded from a local `.env` file automatically on startup.
- Entry passwords are never read from the environment — pass `--password` directly to `add-entry`/`edit-entry`.
## Security
- **Never pass your database password as a CLI argument.** Read it from `KEEPASS_DATABASE_PASSWORD` only, so you don't leak it into shell history, process listings, or shared logs.
- **Entry passwords go through the CLI by design.** `--password` on `add-entry`/`edit-entry` is how you set them; avoid logging command invocations that include one.
- **Don't commit** your `.env` file to version control. Add it to `.gitignore`.
- **Lock down file permissions** on your `.env` file and `.kdbx` database to owner read/write only (`chmod 600`).
- **Ask for secrets only when you need them.** Pass `--show-secrets` only when you actually need the password/OTP value, and avoid echoing that output anywhere it could be logged.
- **Treat `--permanent` as irreversible.** `delete-entry` and `delete-group` land in the Recycle Bin by default — only pass `--permanent` when you deliberately want to skip that safety net.
don't have the plugin yet? install it then click "run inline in claude" again.
automate the full lifecycle of a local KeePass database without touching a UI. create and inspect .kdbx files, manage entries (list, search, add, edit, delete, move), organize groups, and attach files. all operations print structured JSON. use this when you need to programmatically manage secrets, API keys, and credentials as part of a larger automation workflow.
environment variables (load from .env file):
KEEPASS_DATABASE_PATH (required): absolute or relative path to your .kdbx fileKEEPASS_DATABASE_PASSWORD (required): master password to unlock the database. never pass this as a CLI argumentcli dependencies:
python-dotenv (loads .env)pykeepass (wraps the KeePass format)setup before first call:
python -m pip install python-dotenv pykeepass
# create a .env file with KEEPASS_DATABASE_PATH and KEEPASS_DATABASE_PASSWORD
chmod 600 .env # lock down permissions
file inputs (for certain commands):
--database (optional): override KEEPASS_DATABASE_PATH for a single call--file (for add-attachment): local file to attach to an entry--force (optional): skip safety checks on delete-attachment if the binary is sharedselectors (mutually exclusive, pick one per entry or group):
--entry-uuid, --title, --query (substring search)--group (path like "Services/APIs"), --parent-group, --destination-group--attachment-id (from list-attachments output) or --filenameKEEPASS_DATABASE_PATH)python keepass_cli.py create{"ok": true, "action": "create", "database": {"path": "...", "root_group": "Root"}} or {"ok": false, "error_code": "database_exists"} if file already existspython keepass_cli.py create --force to overwrite an existing databasepython keepass_cli.py database-info{"ok": true, "action": "database-info", "database": {"version": "...", "encryption": "...", "entry_count": N, "group_count": N}} with metadata on the open database--query (substring match on title), --group (filter by group path), --sort-by (one of: uuid, title, group, modified; default uuid)python keepass_cli.py list-entries [--query TERM] [--group PATH] [--sort-by FIELD]{"ok": true, "action": "list-entries", "entries": [{"uuid": "...", "title": "...", "group": "...", ...}, ...]} with zero or more entries--show-secrets or --include-metadata--entry-uuid, --title, or --query (mutually exclusive); optional --show-secrets (include password, otp), --include-metadata (include created, modified, accessed, expires)python keepass_cli.py show-entry --title "GitHub" [--show-secrets] [--include-metadata]{"ok": true, "action": "show-entry", "entry": {"uuid": "...", "title": "...", "username": "...", "url": "...", "notes": "...", "group": "...", "attachments": [...], ...}} with all fields--query matches zero entries, return {"ok": false, "error_code": "not_found"}--query or --title matches two or more entries, return {"ok": false, "error_code": "ambiguous_selector"}--title (required), --username (optional), --url (optional), --notes (optional), --password (required), --group (optional, defaults to root), optional --include-metadata (return created, modified, expires timestamps)python keepass_cli.py add-entry --title "GitHub" --username "agent@example.com" --url "https://github.com" --group "Services/APIs" --password "YOUR_PASS"{"ok": true, "action": "add-entry", "entry": {...}} with the new entry details{"ok": false, "error_code": "not_found"}--entry-uuid, --title, or --query), at least one field to change (--username, --url, --notes, --password), optional --include-metadatapython keepass_cli.py edit-entry --title "GitHub" --username "newuser@example.com"{"ok": true, "action": "edit-entry", "entry": {...}} with updated entry{"ok": false, "error_code": "no_fields_provided"}{"ok": false, "error_code": "ambiguous_selector"}--entry-uuid, --title, --query), optional --permanent (skip recycle bin, delete immediately)python keepass_cli.py delete-entry --title "GitHub"{"ok": true, "action": "delete-entry", "entry": {"uuid": "...", "title": "...", ...}} confirming what was deletedpython keepass_cli.py delete-entry --title "GitHub" --permanent to skip the bin--destination-group (required, path like "Archive")python keepass_cli.py move-entry --title "GitHub" --destination-group "Archive"{"ok": true, "action": "move-entry", "entry": {...}} with updated group path{"ok": false, "error_code": "not_found"}--sort-by (one of: uuid, name, path, default uuid)python keepass_cli.py list-groups [--sort-by path]{"ok": true, "action": "list-groups", "groups": [{"uuid": "...", "name": "...", "path": "...", "entry_count": N}, ...]} with all groups--name (required, e.g. "APIs"), --parent-group (optional, path like "Services"; defaults to root)python keepass_cli.py add-group --name "APIs" --parent-group "Services"{"ok": true, "action": "add-group", "group": {"uuid": "...", "name": "APIs", "path": "Services/APIs", ...}}{"ok": false, "error_code": "not_found"}--group (required, path like "Services/APIs"), optional --permanent (default: move to recycle bin)python keepass_cli.py delete-group --group "Services/APIs"{"ok": true, "action": "delete-group", "group": {"uuid": "...", "name": "APIs", "path": "Services/APIs"}}{"ok": false, "error_code": "root_protected"}{"ok": false, "error_code": "not_found"}--group (source path), --destination-group (target parent path)python keepass_cli.py move-group --group "APIs" --destination-group "Archive"{"ok": true, "action": "move-group", "group": {...}} with updated path{"ok": false, "error_code": "root_protected"}{"ok": false, "error_code": "invalid_move"}--entry-uuid, --title, --query)python keepass_cli.py list-attachments --title "GitHub"{"ok": true, "action": "list-attachments", "attachments": [{"id": 0, "filename": "token.pem", "size": 1234, ...}, ...]} with all attachments on that entry--file (path to local file, required)python keepass_cli.py add-attachment --title "GitHub" --file ./token.pem{"ok": true, "action": "add-attachment", "attachment": {"id": 0, "filename": "token.pem", "size": 1234, ...}}{"ok": false, "error_code": "file_not_found"}{"ok": false, "error_code": "not_found"}--attachment-id or --filename, mutually exclusive), optional --delete-binary (remove the underlying binary data), optional --force (override safety checks)python keepass_cli.py delete-attachment --title "GitHub" --attachment-id 0{"ok": true, "action": "delete-attachment", "attachment": {"id": 0, "filename": "token.pem"}}--delete-binary and other entries still reference the same binary, return {"ok": false, "error_code": "shared_resource"} unless you also pass --force{"ok": false, "error_code": "not_found"}if the database file does not exist at KEEPASS_DATABASE_PATH:
create will return {"ok": false, "error_code": "database_not_found"}. call create first.if KEEPASS_DATABASE_PASSWORD is missing or wrong:
{"ok": false, "error_code": "auth_failure"}. verify the env var is set and correct.if KEEPASS_DATABASE_PATH or KEEPASS_DATABASE_PASSWORD is missing entirely:
{"ok": false, "error_code": "missing_env_var", "var": "KEEPASS_DATABASE_PATH"} or similar.if a .kdbx file is corrupted or in an unsupported version:
{"ok": false, "error_code": "database_corrupt"}. you may need to restore from a backup.if an entry or group selector is ambiguous (matches 2+ results):
{"ok": false, "error_code": "ambiguous_selector", "matches": N}. the caller must refine their selector.if a selector matches zero results:
{"ok": false, "error_code": "not_found"}. the resource does not exist or has already been deleted.if an argument is invalid (e.g. unknown --sort-by value):
{"ok": false, "error_code": "invalid_argument", "argument": "sort-by", "reason": "..."}.if a command hits an unexpected Python exception:
{"ok": false, "error_code": "internal_error", "type": "ValueError", "message": "..."} with the exception class and message. log the full traceback to stderr for debugging.if the database hits a rate limit or resource contention (concurrent writes):
if network I/O is required (none by default):
every command returns exactly one JSON object to stdout. the object has this schema:
{
"ok": boolean,
"action": string (e.g. "add-entry", "list-entries"),
"entry"?: object | object[] (present on entry commands),
"group"?: object | object[] (present on group commands),
"attachment"?: object | object[] (present on attachment commands),
"database"?: object (present on database commands),
"error"?: string (human-readable, present if ok=false),
"error_code"?: string (machine-readable, present if ok=false),
"type"?: string (Python exception class, present if error_code="internal_error"),
"message"?: string (exception message, present if error_code="internal_error")
}
exit status:
0: success (ok=true)2: handled error (ok=false, predictable error code)1: unexpected error (ok=false, error_code="internal_error" or crash)error codes (stable across versions, match on these not the human-readable error string):
invalid_argument, invalid_selector, not_found, ambiguous_selector, auth_failure, database_not_found, database_exists, database_corrupt, missing_env_var, root_protected, invalid_move, file_not_found, shared_resource, no_fields_provided, internal_error
secret fields (withheld by default):
password and otp are omitted unless you pass --show-secretscreated, modified, accessed, expires, expiry_time) are omitted unless you pass --include-metadataexample success response:
{
"ok": true,
"action": "add-entry",
"entry": {
"uuid": "b6f1...c2",
"title": "GitHub",
"username": "agent@example.com",
"url": "https://github.com",
"notes": "",
"group": "Services/APIs",
"attachments": []
}
}
example error response:
{
"ok": false,
"action": "show-entry",
"error": "No entry matched the supplied selector.",
"error_code": "not_found"
}
you know the skill worked when:
0 and the response has "ok": true and the expected action fieldlist-* commands, an array under entries, groups, or attachments (may be empty)--show-secrets, the response includes password and otp fields--include-metadata, the response includes timestamp fields like created and modifiedlist-entries, moved group has new path, etc.)human-readable error strings in error are for debugging only. check error_code to decide whether to retry, escalate, or report the problem to the user. if exit status is 1, the skill encountered an unexpected state; check logs and consider this a fatal failure.