From de08846bc0ea6c3b43837d75bcd5c00fd5d4a8d7 Mon Sep 17 00:00:00 2001 From: Maris Popens Date: Thu, 24 Sep 2026 21:42:46 +0300 Subject: [PATCH 1/2] feat: add kp-env and kpxc-cli for KeePassXC-backed secrets kp-env prints export lines for a KeePassXC env/ group, replacing per-repo .env files (eval "$(kp-env proxmox-opentofu)"). It uses kpxc-cli against the running app when available (Touch ID), otherwise keepassxc-cli on the .kdbx. kpxc-cli is installed alongside the keepassxc cask. --- dot_Brewfile.tmpl | 2 + dot_local/bin/executable_kp-env | 67 +++++++++++++++++++++++++++++++++ 2 files changed, 69 insertions(+) create mode 100644 dot_local/bin/executable_kp-env diff --git a/dot_Brewfile.tmpl b/dot_Brewfile.tmpl index 4526e0e..98b4ccd 100644 --- a/dot_Brewfile.tmpl +++ b/dot_Brewfile.tmpl @@ -16,6 +16,7 @@ tap "xykong/tap", trusted: true {{ end -}} {{ if .macos_utils -}} tap "darrylmorley/whatcable", trusted: true +tap "mietzen/tap", trusted: true {{ end -}} # taps:end @@ -232,6 +233,7 @@ brew "macmon" {{ end -}} {{ if not (has "keepassxc" .machine_excludes) -}} cask "keepassxc" +brew "mietzen/tap/keepassxc-cli" {{ end -}} {{ if not (has "notunes" .machine_excludes) -}} cask "notunes" diff --git a/dot_local/bin/executable_kp-env b/dot_local/bin/executable_kp-env new file mode 100644 index 0000000..67ac7f7 --- /dev/null +++ b/dot_local/bin/executable_kp-env @@ -0,0 +1,67 @@ +#!/usr/bin/env python3 +# Print `export VAR=value` lines for every entry in a KeePassXC env/ group +# (entry title = var name, password = value). Replaces per-repo .env files: +# eval "$(kp-env proxmox-opentofu)" +# Uses kpxc-cli (talks to the running KeePassXC app, so Touch ID works) when +# installed; entries need URL https://.kp-env.invalid for that lookup. +# Falls back to keepassxc-cli on the .kdbx (master password prompt). +# DB defaults to ~/Drumandbytes_keepassxc.kdbx; override with KP_DB. +import csv, io, json, os, shlex, shutil, subprocess, sys + + +def export(title, value): + return f"export {title}={shlex.quote(value)}" + + +def from_csv(csv_text, name): + return [export(r["Title"], r["Password"]) for r in csv.DictReader(io.StringIO(csv_text)) + if r["Group"].endswith(f"/env/{name}")] + + +def from_kpxc(json_text, name): + # kpxc-cli -j prints one JSON object per entry, back to back + dec, text, i, out = json.JSONDecoder(), json_text.strip(), 0, [] + while i < len(text): + e, i = dec.raw_decode(text, i) + while i < len(text) and text[i].isspace(): + i += 1 + if e.get("group") == name: + out.append(export(e["name"], e.get("password", ""))) + return out + + +def demo(): + sample = ('"Group","Title","Username","Password"\n' + '"Root/MiniPC/env/repo","A","","x y\'z"\n' + '"Root/MiniPC/env/other","B","","nope"\n') + assert from_csv(sample, "repo") == ["export A='x y'\"'\"'z'"] + assert from_csv(sample, "missing") == [] + js = ('{\n "name": "A", "group": "repo", "password": "p q"\n}\n' + '{\n "name": "B", "group": "other", "password": "no"\n}\n') + assert from_kpxc(js, "repo") == ["export A='p q'"] + print("ok") + + +if __name__ == "__main__": + if sys.argv[1:] == ["--selftest"]: + demo(); sys.exit() + if len(sys.argv) != 2: + sys.exit("usage: eval \"$(kp-env )\"") + name = sys.argv[1] + lines = [] + if shutil.which("kpxc-cli"): + r = subprocess.run(["kpxc-cli", "show", f"https://{name}.kp-env.invalid", "-p", "-j"], + stdout=subprocess.PIPE, text=True) + if r.returncode == 0: + lines = from_kpxc(r.stdout, name) + if not lines: + db = os.path.expanduser(os.environ.get("KP_DB", "~/Drumandbytes_keepassxc.kdbx")) + # password prompt goes to stderr, so stdout stays clean for eval + r = subprocess.run(["keepassxc-cli", "export", "-f", "csv", db], + stdout=subprocess.PIPE, text=True) + if r.returncode: + sys.exit(r.returncode) + lines = from_csv(r.stdout, name) + if not lines: + sys.exit(f"kp-env: no entries in env/{name}") + print("\n".join(lines)) From 5a4f0cb147804e9022d19f2a6a10a5af4759ae47 Mon Sep 17 00:00:00 2001 From: Maris Popens Date: Thu, 24 Sep 2026 22:12:52 +0300 Subject: [PATCH 2/2] docs: fix rendered Brewfile path in CLAUDE.md dot_Brewfile.tmpl renders to ~/.Brewfile, not ~/Brewfile, and apply installs it via run_onchange_brew-bundle. --- CLAUDE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CLAUDE.md b/CLAUDE.md index 68cbc54..58db133 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -7,7 +7,7 @@ This repo is chezmoi's **source** directory, not the live config. chezmoi renders source paths to **target** paths on `~` using its naming convention: - `dot_` prefix → literal `.` in the target. `dot_zshrc.tmpl` → `~/.zshrc`. `dot_config/` → `~/.config/`. `dot_hammerspoon/` → `~/.hammerspoon/`. `dot_zsh/` → `~/.zsh/`. -- `.tmpl` suffix → the file is a Go template chezmoi renders (vars like `{{ if .dev_apps }}`), not literal output. `dot_zshrc.tmpl` → rendered → `~/.zshrc`; `dot_Brewfile.tmpl` → rendered → `~/Brewfile`; `dot_zsh/env.zsh.tmpl` → rendered → `~/.zsh/env.zsh`. +- `.tmpl` suffix → the file is a Go template chezmoi renders (vars like `{{ if .dev_apps }}`), not literal output. `dot_zshrc.tmpl` → rendered → `~/.zshrc`; `dot_Brewfile.tmpl` → rendered → `~/.Brewfile` (installed by `run_onchange_brew-bundle` on apply); `dot_zsh/env.zsh.tmpl` → rendered → `~/.zsh/env.zsh`. So: never edit `~/.zshrc`, `~/.config/...`, or any other rendered file on disk directly — edits get clobbered on the next `chezmoi apply` and never make it back to this repo. Always edit the `dot_*`/`*.tmpl` source file here, then apply. To pull a manual on-disk edit back into source, use `chezmoi re-add ` (or `dots-add`, see below).