build/skills/secrets/SKILL.md
MUST be invoked before any work involving: charly secrets commands, Secret Service / config-file credential management, GPG-encrypted .secrets files, credential import/export, or secret administration.
npx skillsauth add overthinkos/overthink-plugins secretsInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
charly has two distinct secret systems:
charly secrets get/set/list/delete/import/export) — keyed
service/key credentials (VNC passwords, layer secret_* values) resolved
from the Secret Service (system keyring) with a config-file plaintext
fallback for headless hosts. Provisioned into Podman secrets at charly config
time..secrets files (charly secrets gpg …) — project-level shell env vars
in a GPG-encrypted .secrets file, decrypted by direnv on cd.There is no direct KeePass
.kdbxfile backend. KeePassXC is fully supported — but as a Secret Service provider (via its FdoSecrets plugin), i.e. the keyring backend below, not a.kdbxfile charly reads directly. An existing.kdbxserves its secrets with zero data copy once exposed through FdoSecrets. The obsoletesecret_backend: kdbx/secrets_kdbx_*config keys raise a hard load-time error pointing atcharly migrate.
Resolution order (first match wins):
| Priority | Backend | When used |
|----------|---------|-----------|
| 1 | Environment variable | CHARLY_VNC_PASSWORD, etc. |
| 2 | System keyring (Secret Service) | GNOME Keyring, KDE Wallet, KeePassXC (FdoSecrets) |
| 3 | Config file | Plaintext fallback in config.yml (headless last-resort) |
secret_backend ∈ {auto (default; keyring then config), keyring, config}.
Iteration-capable keyring read path: when the system
keyring backend is active, charly's read path (KeyringStore.Get →
keyringGetViaSSClient → ssClient.findItemAnyCollection) does NOT rely on
the Secret Service default alias alone. It iterates all healthy collections,
skipping any that return DBus errors on property reads. This makes charly
resilient to broken default alias stubs (commonly seen in KeePassXC
FdoSecrets setups) — credentials stored in a non-default collection are still
findable. Set keyring_collection_label via /charly-build:settings to pin charly to a
specific collection by label. When every candidate collection is locked
(requires interactive unlock), source=locked is returned and
charly config mount waits indefinitely via event-driven DBus signal
subscription (zero CPU) until the user unlocks the keyring. See /charly-automation:enc
for the full iteration order, source classification, event-driven waiting
behavior, and troubleshooting.
All commands below operate on the active credential store (Secret Service,
or the config-file fallback) — never a .kdbx file.
| Action | Command | Description |
|--------|---------|-------------|
| List entries | charly secrets list [service] | List credentials, optionally filter by service prefix |
| Get value | charly secrets get <service> <key> | Print credential value |
| Set value | charly secrets set <service> <key> [value] | Set credential (prompts if no value) |
| Generate value | charly secrets set <service> <key> --generate | Generate 32-char hex random value |
| Delete entry | charly secrets delete <service> <key> | Remove an entry |
| Import | charly secrets import [--dry-run] | Copy config.yml + keyring credentials into the active store |
| Export | charly secrets export [--format yaml\|json] | Export all entries (plaintext!) |
charly secrets list # All charly entries (keyring shadow index + config)
charly secrets list charly/vnc # Filter by service prefix
Prints service/key pairs, one per line. The listing is the union of the
keyring shadow index (keyring_keys in config.yml) and the config-file
plaintext entries, so it reflects everything regardless of the active backend.
charly secrets get charly/vnc my-image # Print value
charly secrets set charly/vnc my-image # Prompt for value securely
charly secrets set charly/vnc my-image mypassword # Set value inline
charly secrets set charly/vnc my-image --generate # Generate and print random value
charly secrets delete charly/vnc my-image # Remove entry
set/delete operate on the active store (DefaultCredentialStore()); get
reads the active store. The --generate flag produces 16 random bytes as a
32-character hex string, printed to stdout.
charly secrets import --dry-run # Preview what would be imported
charly secrets import # Copy config.yml + keyring entries into the active store
Collects credentials from:
config.ymlkeyring_keys config listCopies (does NOT clear the source) into the active store. This differs from
charly secrets migrate-secrets, which moves config plaintext into the keyring
and strips the plaintext copies.
charly secrets export # YAML format (default)
charly secrets export --format json # JSON format
WARNING: Exports plaintext credentials. Outputs nested map: service -> key -> value. Values are resolved through the active store (with config fallback).
A running Secret Service provider (GNOME Keyring, KDE Wallet, or KeePassXC with
FdoSecrets enabled) is all that's needed — secret_backend: auto finds it. On a
headless host with no Secret Service, charly falls back to the config-file store
automatically.
charly settings set secret_backend keyring # (optional) force the keyring backend
charly secrets set charly/secret MY_TOKEN xyz # store a credential
charly secrets get charly/secret MY_TOKEN # read it back
secret_accept / secret_require)A candy can declare credential-backed env vars in charly.yml via the
secret_accept: / secret_require: sections. At charly config time, the
declared values are resolved from the credential store, provisioned as
per-image podman secrets, and injected into the container at runtime via
Secret=<name>,type=env,target=<var> directives — never landing in
charly.yml or the generated quadlet as plaintext. See /charly-image:layer
(secret_accept / secret_require) for the authoring side.
The credential store namespace for these entries defaults to charly/secret
with the env var name as the key. Layer authors can override with an
explicit key: charly/api-key/openrouter in charly.yml, which is useful when
multiple consumers should resolve the same upstream credential (e.g.,
openwebui and hermes both pointing at charly/api-key/openrouter so one
charly secrets set populates both).
Storage commands:
# Default path (matches charly.yml `secret_accept: [{name: WEBUI_ADMIN_PASSWORD}]`)
charly secrets set charly/secret WEBUI_ADMIN_PASSWORD <password>
# Explicit key path (matches charly.yml `key: charly/api-key/openrouter`)
charly secrets set charly/api-key openrouter sk-or-xxxxxxxx
charly secrets set charly/api-key ollama gsk-yyyyyyyy
charly secrets set charly/api-key immich <immich-key-from-web-ui>
Auto-generated secret_require: tokens.
secret_require: entries that miss everywhere (env + credential
store) auto-generate a 32-byte hex token via generateRandomHex(32) +
DefaultCredentialStore.Set, persisted at charly/secret/<NAME> (or the
declared key: override). The first deploy that resolves the secret
writes; every subsequent deploy reads the persisted value. Race-free
across multi-layer declarations because DefaultCredentialStore caches
via sync.Once — when k3s-server and k3s-agent both declare
K3S_CLUSTER_TOKEN, whichever resolves first writes, and the other
reads. No operator setup required for charly update check-k3s-vm to succeed
on a fresh host.
secret_accept: entries do NOT auto-generate (they're optional by
contract; the caller falls back to dep.Default when missing). Only
secret_require: triggers the auto-gen path.
Retrieve an auto-generated value (e.g., to log into a service for the first time):
charly secrets get charly/secret K3S_CLUSTER_TOKEN
charly secrets get charly/secret WEBUI_ADMIN_PASSWORD
Override a secret_require: value with a specific value before
the first deploy:
charly secrets set charly/secret K3S_CLUSTER_TOKEN <value>
The auto-gen path is skipped whenever any non-empty value is already resolvable (env var, keyring, or config-file fallback).
Persistence venue. When no keyring is available (headless
first-boot, fresh CI runner), the auto-generated token lands in
~/.config/charly/config.yml via ConfigFileStore (mode 0600).
Operators on multi-user hosts should run a Secret Service provider
(e.g. KeePassXC with FdoSecrets enabled) BEFORE the first deploy so
subsequent secrets land in the keyring instead of the plaintext
config file.
Rotation: update the store and re-run charly config. The
RotateOnConfig flag on credential-backed secrets bypasses the
podmanSecretExists short-circuit, so the podman secret is re-created
with the new value on every charly config:
charly secrets set charly/api-key/openrouter <new-value>
charly config openwebui --update-all
systemctl --user restart charly-openwebui.service
One-shot -e import: the -e NAME=VAL CLI flag on charly config
auto-imports the value into the credential store when NAME matches a
secret_accept / secret_require declaration on the target image. The
plaintext is stripped from c.Env before it can reach saveDeployState
or the quadlet writer. First-time setup:
charly config openwebui -e WEBUI_ADMIN_PASSWORD=<password> -e OPENROUTER_API_KEY=sk-or-xxx
Subsequent charly config openwebui resolves from the store without needing
-e again.
Migration from legacy plaintext: charly config automatically moves any
NAME=VAL entry in charly.yml whose NAME is a secret_accept /
secret_require declaration on the image into the credential store.
The plaintext is stripped, charly.yml.bak.<ts> is written as a rollback
point, and the migration logs each entry on stderr. Idempotent — safe to
run on a clean host.
Distinction from layer-owned secret:: the charly.yml secret:
field (e.g., immich's db-password) creates per-image secrets that are
auto-generated once at charly config time and never rotated. Credential-
backed secret_accept / secret_require are user-owned, shareable
across consumers, and refreshed on every charly config. Both flow through
the same Secret=<name>,type=env,target=<var> quadlet emission; only the
rotation semantics differ. See /charly-image:layer (secret_accept / secret_require).
| Key | Description |
|-----|-------------|
| secret_backend | Force backend: auto (default; keyring then config), keyring, or config |
| keyring_collection_label | Preferred Secret Service collection label (empty = iterate naturally). Pin charly to a specific collection in multi-database setups. |
Separate from charly's credential store, project-level environment variables (e.g., GMAIL_USER, GMAIL_PASSWORD) are stored in .secrets — a GPG-encrypted file at the project root. direnv decrypts it in memory via charly secrets gpg env when entering the directory (eval "$(charly secrets gpg env)" in .envrc).
This is NOT managed by the charly secrets credential store. The two systems serve different purposes:
| System | What it manages | How it works |
|--------|----------------|--------------|
| charly secrets (Secret Service / config) | Container-level credentials (VNC passwords, service secrets) | Provisioned at charly config time into Podman secrets |
| charly secrets gpg + direnv | Project-level shell env vars (API keys, credentials) | GPG-encrypted .secrets file, decrypted by direnv on cd |
charly secrets gpg — GPG-Encrypted .secrets ManagementManage GPG-encrypted .secrets environment files directly from the CLI. All commands shell out to gpg (must be in PATH).
| Action | Command | Description |
|--------|---------|-------------|
| Show contents | charly secrets gpg show [-f FILE] | Decrypt and print to stdout |
| Export for check | charly secrets gpg env [-f FILE] | Decrypt .secrets as export KEY='value' for check/direnv |
| Edit in editor | charly secrets gpg edit [-f FILE] | Decrypt, open $EDITOR, re-encrypt |
| Encrypt file | charly secrets gpg encrypt -r KEY_ID [-i .env] [-o .secrets] | Encrypt plaintext env file |
| Decrypt file | charly secrets gpg decrypt [-i .secrets] [-o FILE] | Decrypt to file or stdout |
| Set a key | charly secrets gpg set KEY VALUE [-f FILE] [-r KEY_ID] | Add or update KEY=VALUE |
| Remove a key | charly secrets gpg unset KEY [-f FILE] | Remove a key from .secrets |
| Add recipient | charly secrets gpg add-recipient KEY_ID [-f FILE] | Re-encrypt with additional recipient |
| List recipients | charly secrets gpg recipients [-f FILE] | List GPG key IDs that can decrypt |
| Import key | charly secrets gpg import-key <path> | Import GPG key from file, directory, or Secret Service |
| Export key | charly secrets gpg export-key [path] [--to-keystore] | Export GPG key to directory and/or KeePassXC |
| Setup | charly secrets gpg setup | Configure gpg-agent, import/generate key, store passphrase |
| Doctor | charly secrets gpg doctor [-f FILE] | Health check: GPG agent, keys, Secret Service, .secrets |
charly secrets gpg envDecrypt .secrets and output export KEY='value' lines to stdout. Designed for eval or direnv:
eval "$(charly secrets gpg env)" # Load secrets into current shell
eval "$(charly secrets gpg env -f .secrets.prod)" # Load from specific file
Behavior:
.envrc — no error when .secrets is absent)shellQuote)dotenv_gpg_if_exists direnvrc function — no external dependency neededUsage in .envrc:
eval "$(charly secrets gpg env)"
# Create .secrets from a plaintext .env file
charly secrets gpg encrypt -r 420DE2B3 -i .env -o .secrets
rm .env
# View contents
charly secrets gpg show
# Add a new secret
charly secrets gpg set API_KEY sk-test-abc123
# Edit in your editor
charly secrets gpg edit
# Remove a secret
charly secrets gpg unset OLD_KEY
# Add another person who can decrypt
charly secrets gpg add-recipient THEIR_KEY_ID
# See who can decrypt
charly secrets gpg recipients
-f, --file — Path to encrypted file (default: .secrets in current directory)-r, --recipient — GPG key ID (repeatable, required for encrypt, optional for set on new files)-i, --input — Input file for encrypt/decrypt (default: .env / .secrets)-o, --output — Output file for encrypt/decrypt (default: .secrets / stdout)charly secrets gpg import-key — Import GPG KeysImport from file, directory, or KeePassXC Secret Service:
charly secrets gpg import-key ~/Sync/Conf/gpg/ # From directory (all .asc/.gpg + ownertrust.txt)
charly secrets gpg import-key ~/key.asc # From single file
charly secrets gpg import-key --from-keystore # From KeePassXC Secret Service
charly secrets gpg import-key --from-keystore --key-id XX # Specific key from KeePassXC
charly secrets gpg import-key ~/keys/ --passphrase "xxx" # With loopback pinentry (no GUI prompt)
Directory import auto-detects .asc, .gpg, and ownertrust.txt files. Keystore import retrieves armored keys stored with schema org.gnupg.Key — these are created by charly secrets gpg export-key --to-keystore.
The --passphrase flag uses GPG's loopback pinentry mode, avoiding the GUI prompt during import.
charly secrets gpg export-key — Export GPG KeysExport to filesystem and/or KeePassXC:
charly secrets gpg export-key ~/Sync/Conf/gpg/ # To directory (public.asc, secret.asc, ownertrust.txt)
charly secrets gpg export-key --to-keystore # To KeePassXC Secret Service
charly secrets gpg export-key ~/backup/ --to-keystore # Both filesystem and KeePassXC
charly secrets gpg export-key --key-id XXXX --to-keystore # Specific key
charly secrets gpg export-key --to-keystore --passphrase X # Also store passphrase in Secret Service
Keystore export stores the armored private key as a Secret Service entry (schema org.gnupg.Key, attributes: keyid, uid). This enables key recovery via import-key --from-keystore without filesystem backups.
charly secrets gpg setup — One-Stop GPG ConfigurationConfigures the full GPG/KeePassXC chain in one command:
charly secrets gpg setup # Interactive
charly secrets gpg setup --import ~/Sync/Conf/gpg/ # Import key, then configure
charly secrets gpg setup --from-keystore # Restore key from KeePassXC
charly secrets gpg setup --passphrase "xxx" # Batch mode (no prompts)
Steps performed:
gpg, gpg-connect-agent, pinentry-qt (libsecret), secret-tool~/.gnupg/gpg-agent.conf (pinentry-qt, 8h cache, allow-preset-passphrase)gpg-agent.socket, gpg-agent-extra.socket)Flags:
--import <path> — Import key from file/directory before setup--from-keystore — Import key from KeePassXC before setup--passphrase <value> — Batch mode: generate key + store passphrase without prompts--key-id <id> — Use specific existing key--skip-secret-service — Skip passphrase storage in Secret Servicecharly secrets gpg doctor — Health CheckRead-only diagnostic of the full GPG/direnv chain:
charly secrets gpg doctor # Check everything
charly secrets gpg doctor -f .secrets.prod # Check specific file
Checks: gpg binary, gpg-agent status, gpg-agent.conf (pinentry, cache TTL), systemd sockets, secret keys, Secret Service availability, passphrase storage for each keygrip, key backups in Secret Service, .secrets file recipients and decrypt test.
Returns non-zero exit code if any critical issue found.
gpg --decrypt .secrets
→ gpg-agent (needs passphrase for subkey keygrip)
→ spawns pinentry-qt
→ pinentry-qt: secret_password_lookup_nonpageable_sync()
schema: "org.gnupg.Passphrase", attr: keygrip="<40-char-hex>"
→ KeePassXC returns stored passphrase (if found) → no GUI prompt
→ OR: pinentry-qt shows GUI dialog → user enters passphrase
→ gpg-agent caches passphrase (8h default, 12h max)
→ decryption succeeds
Key facts:
org.freedesktop.secrets D-Bus APIorg.gnupg.Passphrase with attribute keygripcharly secrets gpg setup stores passphrases for all keygrips automaticallyTwo Secret Service schemas:
org.gnupg.Passphrase — passphrase per keygrip (compatible with pinentry-qt auto-lookup)org.gnupg.Key — armored private key backup (for import-key --from-keystore recovery)Both are standard Secret Service entries visible in KeePassXC's FdoSecrets-exposed group.
When decryption fails, charly secrets gpg now prints actionable diagnostics instead of raw GPG errors:
charly secrets gpg commands to fix each issue# Option A: Import existing key from backup
charly secrets gpg setup --import ~/Sync/Conf/gpg/
# Option B: Restore from KeePassXC (if key was previously exported)
charly secrets gpg setup --from-keystore
# Option C: Generate fresh key
charly secrets gpg setup
# Then re-encrypt .secrets: charly secrets gpg encrypt -r <NEW_KEY_ID> -i .env -o .secrets
charly secrets gpg export-key --to-keystore # Key backup
charly secrets gpg export-key ~/Sync/Conf/gpg/ # Filesystem backup too
charly secrets gpg import-key --from-keystore # From KeePassXC
charly secrets gpg setup # Configure agent + store passphrase
| Symptom | Cause | Fix |
|---------|-------|-----|
| gpg: No secret key | Key not in keyring | charly secrets gpg import-key <path> |
| Constant passphrase prompts | Passphrase not in Secret Service | charly secrets gpg setup (stores for all keygrips) |
| decrypting .secrets failed | Diagnostics printed automatically | Follow suggested charly secrets gpg commands |
| Secret Service not available | KeePassXC not running/unlocked | Start KeePassXC, unlock database |
| secret-tool store fails | No FdoSecrets exposed group | KeePassXC → right-click group → "Mark as Secret Service exposed" |
| Passphrase stored but still prompted | Stored for wrong keygrip | charly secrets gpg doctor shows which keygrips need passphrases |
For GPG agent forwarding into containers (so gpg --decrypt works inside), use the agent-forwarding layer. See /charly-distros:agent-forwarding for details. The container's GPG uses the host's agent via a forwarded socket — no GPG agent runs inside the container.
/charly-core:charly-config — secret_backend and other settings keys/charly-automation:enc — encrypted volume credential lookup, iteration-capable ssClient, broken-collection troubleshooting, source classification (env/keyring/config/locked/unavailable/default)/charly-build:settings — keyring_collection_label, secret_backend, and other runtime config keys/charly-core:charly-doctor — Secret Service collection health + shadow index consistency checks/charly-core:service — container secrets (secrets field in charly.yml, provisioned at charly config)/charly-distros:agent-forwarding — SSH/GPG agent forwarding into containers/charly-infrastructure:gnupg — GnuPG package layer/charly-openwebui:openwebui — two-tier secrets pattern: podman secrets (WEBUI_SECRET_KEY auto-generated) + GPG .secrets (API keys)/charly-coder:direnv — direnv environment loaderThe credential store + the charly secrets CLI are EXTERNALIZED into the out-of-process
candy/plugin-secrets plugin (the C2 dep-shed that removed go-keyring from
charly/go.mod): candy/plugin-secrets/command.go (the charly secrets CLI: list/get/set/
delete/import/export/migrate-secrets), candy/plugin-secrets/secrets_gpg.go (GPG .secrets
commands + key management + diagnostics), candy/plugin-secrets/{store,config_store, credential_keyring,credential_config,secret_service,verb_credential,credential_admin}.go
(the verb:credential backends + the env-less resolve + the doctor health probe). charly's
core keeps only the thin adapter charly/credential_plugin.go (CredentialStore /
ResolveCredential / DefaultCredentialStore → verb:credential) + charly/secrets.go
(podman-secret provisioning). On an
installed host the plugin lives beside charly at /usr/lib/charly/plugins (every native
package build — the arch PKGBUILD and the fedora .rpm / debian .deb builds — bakes the
binary + its .providers manifest, off the shared pkg/host-command-plugins.txt set).
MUST be invoked when the task involves charly secrets commands, Secret Service / config-file credential management, GPG-encrypted .secrets file management, GPG key management, Secret Service integration, or credential import/export. Invoke this skill BEFORE reading source code or launching Explore agents.
tools
Use when authoring or modifying a charly PLUGIN — a candy with a `plugin:` block that contributes Providers (verbs/kinds/deploy-targets/steps/builders/commands), its own CUE schema, builtin (compiled-in) or external (out-of-tree git repo). Covers the unified Provider model, the per-plugin CUE-schema contract (single source → Go params for dev + schema-over-Describe RPC for runtime), the SDK, and the loader.
tools
The CUE data-validation / configuration CLI (cue), pinned to v0.16.1. Use when working with the cue candy, installing the cue binary into a box or onto a target:local dev host, or running the offline schema-vendoring pipeline that feeds charly's egress validation.
tools
CUE EGRESS validation — validating (and, where it adds value, generating) the config files charly WRITES to a system BEFORE the bytes hit disk. MUST be invoked before working on charly/egress.go, the vendored schemas under candy/plugin-egress/egress-schemas/vendor/, the ValidateEgress / registerVendoredEgressKind path, the offline `task cue:vendor` pipeline, or adding an egress schema for any written artifact (cloud-init, k8s manifests, traefik routes, runtime config, install ledger, systemd/quadlet units, ssh_config, libvirt XML).
tools
Kubernetes cluster-probe declarative check verb — the `kube:` check verb (nodes, pods, ingress, storage class, addon health, apply/delete, and arbitrary resource GETs) served out-of-process by the candy/plugin-kube plugin (vendored client-go; no external kubectl required).