diff --git a/README.md b/README.md
index 086140a..7f317f6 100644
--- a/README.md
+++ b/README.md
@@ -222,6 +222,50 @@ The push credential for the gitea remote is stored in OpenBAO KV
Rotate the PAT by re-running `store-gitea-cred.sh`. The helper token is a
periodic, read-only token scoped to just that one KV path.
+## Personal credential backup — two playground scenarios
+
+Two isolated setups to evaluate using OpenBAO as a *backup* for personal
+passwords. **Both are deliberately excluded from the `admin` policy** (only
+root is break-glass), so your day-to-day admin token cannot read them.
+
+**Scenario A — structured KV** (`personal/` KV v2, versioned):
+
+```bash
+scripts/personal-put.sh github # prompts user/password(hidden)/url
+scripts/personal-list.sh
+scripts/personal-get.sh github # all fields
+scripts/personal-get.sh github password # one field
+```
+
+**Scenario B — transit-encrypted blob** (`transit-personal/` key
+`personal-backup`; OpenBAO holds only the key, the ciphertext is portable):
+
+```bash
+scripts/secret-encrypt.sh passwords-export.csv # -> passwords-export.csv.vaultenc
+scripts/secret-decrypt.sh passwords-export.csv.vaultenc out.csv
+```
+
+Scoped tokens live in `~/.config/openbao/{personal-kv,personal-transit}.token`.
+Trade-offs discussed inline; A = granular/versioned but OpenBAO holds plaintext,
+B = OpenBAO holds only ciphertext (store the blob anywhere offsite).
+
+## Consuming a secret from a system (AppRole)
+
+How a *service* should authenticate (vs. a static token on disk): it holds a
+**RoleID** (non-secret) + **SecretID** (secret), logs in for a **short-lived**
+token, then reads. Demo role `demo-app` can read only `secret/demo-app/*`.
+
+```bash
+scripts/app-get-secret.sh demo-app/config
+# authenticated: token ttl=1200s policies=["default","demo-app"]
+# { "api_key": "...", "db_url": "..." }
+```
+
+RoleID/SecretID in `~/.config/openbao/approle/`. The minted token is 20m TTL and
+scoped to `demo-app` only — proven denied on other paths. This is the
+recommended pattern for real services; the static-token helpers elsewhere in
+this repo are the simpler home-lab shortcut.
+
## Backups (Raft snapshots) — automated
`scripts/backup-raft-snapshots.sh` snapshots **both** instances and prunes to
diff --git a/scripts/app-get-secret.sh b/scripts/app-get-secret.sh
new file mode 100755
index 0000000..1e1f31c
--- /dev/null
+++ b/scripts/app-get-secret.sh
@@ -0,0 +1,23 @@
+#!/usr/bin/env bash
+# AppRole demo: how a *system* consumes a secret.
+# It holds a RoleID (non-secret) + SecretID (secret), logs in to get a
+# SHORT-LIVED token, then reads a secret with it. Contrast with the static-token
+# helpers (git-credential-openbao.sh etc.) that keep a long-lived token on disk.
+#
+# app-get-secret.sh e.g. app-get-secret.sh demo-app/config
+set -euo pipefail
+ADDR="${BAO_ADDR:-http://127.0.0.1:8200}"
+DIR="${OPENBAO_APPROLE_DIR:-$HOME/.config/openbao/approle}"
+path="${1:?usage: app-get-secret.sh }"
+RID="$(cat "$DIR/role_id")"; SID="$(cat "$DIR/secret_id")"
+
+# 1) authenticate -> short-lived token
+login="$(curl -sS --data "$(RID="$RID" SID="$SID" python3 -c \
+ 'import json,os;print(json.dumps({"role_id":os.environ["RID"],"secret_id":os.environ["SID"]}))')" \
+ "$ADDR/v1/auth/approle/login")"
+tok="$(printf '%s' "$login" | jq -r '.auth.client_token // empty')"
+[ -n "$tok" ] || { echo "login failed: $(printf '%s' "$login" | jq -c '.errors // .')" >&2; exit 1; }
+echo "authenticated: token ttl=$(printf '%s' "$login" | jq -r '.auth.lease_duration')s policies=$(printf '%s' "$login" | jq -c '.auth.token_policies')" >&2
+
+# 2) read the secret with that token
+curl -sS -H "X-Vault-Token: $tok" "$ADDR/v1/secret/data/$path" | jq '.data.data'
diff --git a/scripts/personal-get.sh b/scripts/personal-get.sh
new file mode 100755
index 0000000..c923875
--- /dev/null
+++ b/scripts/personal-get.sh
@@ -0,0 +1,18 @@
+#!/usr/bin/env bash
+# Scenario A: read a personal credential back.
+# personal-get.sh # show all fields
+# personal-get.sh password # print one field (for piping)
+set -euo pipefail
+ADDR="${BAO_ADDR:-http://127.0.0.1:8200}"
+TOKF="$HOME/.config/openbao/personal-kv.token"
+name="${1:?usage: personal-get.sh [field]}"; field="${2:-}"
+[ -r "$TOKF" ] || { echo "no token at $TOKF" >&2; exit 1; }
+
+resp="$(curl -sS -H "X-Vault-Token: $(cat "$TOKF")" "$ADDR/v1/personal/data/$name")"
+if printf '%s' "$resp" | jq -e '.errors and (.errors|length>0)' >/dev/null 2>&1; then
+ echo "not found or no access: $name" >&2; exit 1; fi
+if [ -n "$field" ]; then
+ printf '%s' "$resp" | jq -r ".data.data.$field // empty"
+else
+ printf '%s' "$resp" | jq -r '.data.data | to_entries[] | "\(.key): \(.value)"'
+fi
diff --git a/scripts/personal-list.sh b/scripts/personal-list.sh
new file mode 100755
index 0000000..9c2d5cf
--- /dev/null
+++ b/scripts/personal-list.sh
@@ -0,0 +1,8 @@
+#!/usr/bin/env bash
+# Scenario A: list the names stored in the personal/ KV engine.
+set -euo pipefail
+ADDR="${BAO_ADDR:-http://127.0.0.1:8200}"
+TOKF="$HOME/.config/openbao/personal-kv.token"
+[ -r "$TOKF" ] || { echo "no token at $TOKF" >&2; exit 1; }
+curl -sS -H "X-Vault-Token: $(cat "$TOKF")" "$ADDR/v1/personal/metadata?list=true" \
+ | jq -r '.data.keys[]? // empty'
diff --git a/scripts/personal-put.sh b/scripts/personal-put.sh
new file mode 100755
index 0000000..9954b04
--- /dev/null
+++ b/scripts/personal-put.sh
@@ -0,0 +1,19 @@
+#!/usr/bin/env bash
+# Scenario A (structured KV): store a personal credential in the isolated
+# `personal/` KV v2 engine (NOT readable by the day-to-day admin policy).
+# personal-put.sh e.g. personal-put.sh github
+set -euo pipefail
+ADDR="${BAO_ADDR:-http://127.0.0.1:8200}"
+TOKF="$HOME/.config/openbao/personal-kv.token"
+name="${1:?usage: personal-put.sh }"
+[ -r "$TOKF" ] || { echo "no token at $TOKF" >&2; exit 1; }
+
+read -rp "username: " u
+read -rsp "password (hidden): " p; echo
+read -rp "url/notes (optional): " n
+
+payload="$(U="$u" P="$p" N="$n" python3 -c \
+ 'import json,os;print(json.dumps({"data":{"username":os.environ["U"],"password":os.environ["P"],"url":os.environ["N"]}}))')"
+code="$(printf '%s' "$payload" | curl -sS -o /dev/null -w '%{http_code}' \
+ -H "X-Vault-Token: $(cat "$TOKF")" --data @- "$ADDR/v1/personal/data/$name")"
+case "$code" in 200|204) echo "stored personal/$name (versioned)";; *) echo "FAILED (HTTP $code)"; exit 1;; esac
diff --git a/scripts/secret-decrypt.sh b/scripts/secret-decrypt.sh
new file mode 100755
index 0000000..0992d43
--- /dev/null
+++ b/scripts/secret-decrypt.sh
@@ -0,0 +1,19 @@
+#!/usr/bin/env bash
+# Scenario B: decrypt a transit blob produced by secret-encrypt.sh.
+# secret-decrypt.sh [outfile] (default outfile: w/o .vaultenc, +.dec)
+set -euo pipefail
+ADDR="${BAO_ADDR:-http://127.0.0.1:8200}"
+TOKF="$HOME/.config/openbao/personal-transit.token"
+KEY="personal-backup"
+in="${1:?usage: secret-decrypt.sh [outfile]}"
+out="${2:-${in%.vaultenc}.dec}"
+[ -r "$TOKF" ] || { echo "no token at $TOKF" >&2; exit 1; }
+[ -r "$in" ] || { echo "no input file: $in" >&2; exit 1; }
+
+b64="$(cat "$in" \
+ | python3 -c 'import json,sys;print(json.dumps({"ciphertext":sys.stdin.read().strip()}))' \
+ | curl -sS -H "X-Vault-Token: $(cat "$TOKF")" --data @- "$ADDR/v1/transit-personal/decrypt/$KEY" \
+ | jq -r '.data.plaintext // empty')"
+[ -n "$b64" ] || { echo "decrypt failed (wrong key/blob?)" >&2; exit 1; }
+printf '%s' "$b64" | base64 -d > "$out"
+echo "decrypted $in -> $out"
diff --git a/scripts/secret-encrypt.sh b/scripts/secret-encrypt.sh
new file mode 100755
index 0000000..3df984c
--- /dev/null
+++ b/scripts/secret-encrypt.sh
@@ -0,0 +1,20 @@
+#!/usr/bin/env bash
+# Scenario B (transit blob): encrypt a file with OpenBAO's transit key
+# `personal-backup`. OpenBAO holds only the KEY; the ciphertext is a portable
+# blob you can store anywhere (Synology, git, offsite). Output is ASCII.
+# secret-encrypt.sh [outfile] (default outfile: .vaultenc)
+set -euo pipefail
+ADDR="${BAO_ADDR:-http://127.0.0.1:8200}"
+TOKF="$HOME/.config/openbao/personal-transit.token"
+KEY="personal-backup"
+in="${1:?usage: secret-encrypt.sh [outfile]}"; out="${2:-$in.vaultenc}"
+[ -r "$TOKF" ] || { echo "no token at $TOKF" >&2; exit 1; }
+[ -r "$in" ] || { echo "no input file: $in" >&2; exit 1; }
+
+ct="$(base64 -w0 "$in" \
+ | python3 -c 'import json,sys;print(json.dumps({"plaintext":sys.stdin.read()}))' \
+ | curl -sS -H "X-Vault-Token: $(cat "$TOKF")" --data @- "$ADDR/v1/transit-personal/encrypt/$KEY" \
+ | jq -r '.data.ciphertext // empty')"
+[ -n "$ct" ] || { echo "encrypt failed" >&2; exit 1; }
+printf '%s\n' "$ct" > "$out"
+echo "encrypted $in -> $out ($(wc -c < "$out") bytes ciphertext)"