Compare commits

...
4 Commits
Author SHA1 Message Date
noconnorandClaude Opus 5.5 41a90a10fb zsh: verify opc/opd against the vault and explain failures
op item edit can exit non-zero with "(404) Not Found" after an edit it
did save (seen with op 2.34.1 and 2.39.0 on an existing item), which
made opc report failure and skip the ops refresh. opc now reads the
field back and opd re-checks the item's labels, then prints either the
op:// reference it changed or what failed, op's message without its
timestamp prefix, and where to look next.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 16:49:10 +00:00
noconnorandClaude Opus 5.5 5f0dec5270 zsh: let opc take NAME=VALUE and prompt when the value is missing
`opc NAME=VALUE` sets a field directly; `opc NAME` or `opc NAME=` prompts
with hidden input, and an empty answer changes nothing. The old
`opc NAME VALUE` form still works.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 16:43:27 +00:00
noconnorandClaude Opus 5.5 fe8b9a3ec0 claude-obsidian: drop committed bytecode; ignore __pycache__
The claude-obsidian merge carried a CPython 3.12 .pyc from running the
sync script in place. Bytecode is a build artifact; ignore it repo-wide.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 13:10:49 +00:00
noconnorandClaude Opus 5 72d85eb9ec vscode: install VS Code into ~/.local/opt from upstream's tarball
The box this was written on had a half-finished install: vscode.repo
present under /etc/yum.repos.d, `code` never installed, and nothing in
this repo recording that VS Code was meant to be there. Microsoft's
rpm/deb would also need root, write outside $HOME, and be wiped by a
Kasm redeploy. Upstream's portable tarball is the same build with none
of that, so the module keeps bin/'s contract: no sudo, nothing outside
$HOME, pinned rather than floating.

The Chromium sandbox survives a userland install — the bundled
chrome-sandbox is not setuid, so Electron falls back to the
unprivileged user-namespace sandbox. Verified by launching without
--no-sandbox, so dropping root costs nothing.

Adds install_tarball_tree to lib/fetch.sh: install_tarball_bin picks a
single binary out of an archive, and VS Code only runs from an intact
Electron tree. It stages beside the target and swaps in after
extraction succeeds, so an interrupted run leaves the old version
intact.

Two things the verification caught rather than the design:

  - argv.json lives at ~/.vscode/argv.json (product.json's
    dataFolderName), not under the user-data-dir. A copy written
    anywhere else is read by nothing, so the first attempt at
    suppressing the keyring prompt silently did nothing.

  - Run as root, `code --version` prints a "super user" warning
    instead of the version and still exits 0, so a bare run-check
    never matches and would re-download 325MB on every bootstrap.
    Root falls back to the .versions stamp.

Sets password-store=basic in argv.json so Electron's safeStorage stops
waking kwalletd, which prompts for an unlock on every launch because
kwallet-pam is not installed. Confirmed with dbus-monitor: no
org.kde.KWallet traffic on launch. The trade-off — tokens obfuscated
on disk rather than sealed behind a wallet password — and the route
back to the wallet are documented in the module and the README.

No-ops on headless hosts; DOTFILES_VSCODE overrides either way.

Verified per verify-module: shellcheck -s sh clean, idempotent re-run
on both paths, and fresh install plus re-run as root on oraclelinux:9
and debian:13.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-28 13:08:49 +00:00
7 changed files with 310 additions and 11 deletions
+2
View File
@@ -0,0 +1,2 @@
__pycache__/
*.pyc
+20
View File
@@ -78,6 +78,14 @@ Already have a clone? Just run it directly:
`~/.local/bin`. Persists across redeploys; only rebuilt when the venv is
missing or broken. Building it may install `python3`/`python3-venv` via
the system package manager (the one place this module uses sudo).
- `vscode/` — installs Visual Studio Code from upstream's portable tarball into
`~/.local/opt/vscode`, symlinks `code` into `~/.local/bin`, and writes a
user-level `.desktop` entry so it shows up in the application menu. **No
sudo, nothing outside `$HOME`** — the same contract as `bin/`, so it survives
a Kasm redeploy. Pinned via `VSCODE_VERSION` in the module; bump it to upgrade.
Also sets `password-store: basic` in `~/.vscode/argv.json` so KDE's kwallet
does not prompt for an unlock on every launch (see below). **No-ops on
headless hosts** — it's a ~400MB GUI app — override with `DOTFILES_VSCODE`.
- `docker-rootless-cgroup/` — on systemd-less hosts (e.g. Kasm), hand-delegates
cgroup v2 controllers to the rootless Docker daemon so per-container cgroups
work (rootless Docker otherwise comes up with
@@ -111,6 +119,18 @@ item instead of `~/.zshrc.local`:
don't set this by hand: run `mcp-config/install.sh` once and its shell
hook exports it from `~/.config/mcp-sync/key`. Set it in `~/.zshrc.local`
only on a box without mcp-sync.
- `DOTFILES_VSCODE` — controls the `vscode/` module. Unset (default) installs
VS Code only when a graphical session is detected; `1` forces the install on
a headless host (e.g. for `code tunnel`); `0` skips it everywhere. A plain
shell/env var, not a vault secret.
On the keyring: VS Code's secret storage reaches for the OS keyring through
libsecret, which on KDE makes kwalletd prompt for an unlock before the editor
is usable — every session, since `kwallet-pam` is not installed. The module
sets `"password-store": "basic"` in `~/.vscode/argv.json` to stop that, which
means VS Code's stored tokens are obfuscated on disk rather than sealed
behind a wallet password. To use the wallet instead, install `kwallet-pam` so
it unlocks at login, then change that value to `"kwallet5"`.
- `OP_SERVICE_ACCOUNT_TOKEN` — 1Password service-account token. Not a shell
variable any more: `secrets/install.sh` asks for it once and stores it in
`~/.config/dotfiles/op.env`. Pass it in the env only for an unattended run.
+4
View File
@@ -124,6 +124,10 @@ else
run_module hermes
fi
run_module ansible
# GUI editor, installed from upstream's tarball into ~/.local/opt (no sudo, same
# contract as bin/). No ordering constraints — it needs nothing from bin/,
# secrets/ or node/ — and no-ops on headless hosts.
run_module vscode
# Runs last: needs rootless Docker already up (started at boot) so it can
# delegate cgroups to it. No-ops where that does not apply.
run_module docker-rootless-cgroup
+30 -2
View File
@@ -8,8 +8,8 @@
# reset.
#
# Source this (callers already run `set -eu`), then use fetch_arch /
# fetch_arch_deb, have_version + mark_version, install_tarball_bin, and
# install_raw_bin.
# fetch_arch_deb, have_version + mark_version, install_tarball_bin,
# install_tarball_tree, and install_raw_bin.
BIN_DIR="${BIN_DIR:-$HOME/.local/bin}"
VERSION_DIR="$BIN_DIR/.versions"
@@ -93,6 +93,34 @@ install_zip_bin() {
fsay "installed $BIN_DIR/$3"
}
# install_tarball_tree <url> <root-within-archive> <dest-dir> — download a
# .tar.gz that holds a whole application directory rather than one binary, and
# swap it into <dest-dir>. install_tarball_bin cannot be reused here: VS Code
# is an Electron bundle of thousands of files that only runs from an intact
# tree, not a single binary that can be picked out of the archive.
#
# The new tree is staged beside the target and moved into place only once the
# download and extraction have both succeeded, so an interrupted run leaves the
# previously installed version intact instead of a half-extracted directory.
install_tarball_tree() {
_dest="$3"
_tmp="$(mktemp -d)"
_fetch_dl "$1" "$_tmp/archive.tar.gz"
tar -xzf "$_tmp/archive.tar.gz" -C "$_tmp"
if [ ! -d "$_tmp/$2" ]; then
fsay "fetch: '$2' not found in archive"; rm -rf "$_tmp"; return 1
fi
mkdir -p "$(dirname "$_dest")"
rm -rf "$_dest.new" "$_dest.old"
mv "$_tmp/$2" "$_dest.new"
if [ -e "$_dest" ]; then
mv "$_dest" "$_dest.old"
fi
mv "$_dest.new" "$_dest"
rm -rf "$_dest.old" "$_tmp"
fsay "installed $_dest"
}
# install_raw_bin <url> <dest-name> — download a single raw binary.
install_raw_bin() {
mkdir -p "$BIN_DIR"
+216
View File
@@ -0,0 +1,216 @@
#!/usr/bin/env sh
# vscode/install.sh — install Visual Studio Code into ~/.local/opt/vscode.
#
# WHY: VS Code is the one GUI app this repo installs, and the obvious way to get
# it — Microsoft's rpm/deb repo — needs root, writes outside $HOME, and is wiped
# by a Kasm redeploy, so the editor quietly disappears on every rebuild. The box
# this was written for had exactly that half-state: /etc/yum.repos.d/vscode.repo
# present, `code` never installed, and nothing in this repo recording that VS
# Code was meant to be there at all. Upstream's portable tarball is the same
# build with none of that: everything lands under $HOME, no sudo, and it
# survives a redeploy. The only thing given up is dnf/apt-driven updates, which
# is what the pinned VSCODE_VERSION below replaces.
#
# It is not in bin/install.sh because that script installs single self-contained
# binaries; VS Code is an Electron tree of thousands of files plus a menu entry
# and an icon. It follows node/ and ansible/ instead: unpack under ~/.local/opt
# and symlink the CLI into ~/.local/bin.
#
# The Chromium sandbox still works from a userland install: the bundled
# chrome-sandbox helper is not setuid here, so Electron falls back to the
# unprivileged user-namespace sandbox. That needs user namespaces enabled
# (user.max_user_namespaces > 0, the default on every distro this repo targets).
# Verified by launching without --no-sandbox. Nothing is given up for dropping
# root; if a host ever has userns disabled, VS Code says so on startup.
#
# Idempotent and safe to re-run. To upgrade, bump VSCODE_VERSION.
set -eu
SCRIPT_DIR="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)"
REPO_ROOT="$(dirname -- "$SCRIPT_DIR")"
# shellcheck source=../lib/fetch.sh
. "$REPO_ROOT/lib/fetch.sh"
say() { printf '%s\n' "$*" >&2; }
# Pinned deliberately. VS Code auto-updates itself on other platforms; a tarball
# install does not, which is the point — the editor runs your code and holds
# your Git and extension credentials, so the version is a supply-chain decision
# rather than whatever upstream served today. `code --version` prints this
# string verbatim, so it must match with no leading "v".
VSCODE_VERSION="1.139.0"
PREFIX="$HOME/.local/opt/vscode"
DESKTOP_DIR="$HOME/.local/share/applications"
ARGV_JSON="$HOME/.vscode/argv.json"
# has_desktop — true when this host has a graphical session to run a GUI editor
# in. VS Code is ~400MB unpacked and $HOME is the thing that persists across
# redeploys, so it is not worth carrying on a box that will never display it.
# Desktop images ship session files under /usr/share/{x,wayland-}sessions.
has_desktop() {
[ -n "${XDG_CURRENT_DESKTOP:-}" ] && return 0
for d in /usr/share/xsessions /usr/share/wayland-sessions; do
[ -d "$d" ] && [ -n "$(ls -A "$d" 2>/dev/null)" ] && return 0
done
return 1
}
# --- skip on headless hosts --------------------------------------------------
# DOTFILES_VSCODE=1 forces the install anyway (e.g. for `code tunnel` on a
# server); DOTFILES_VSCODE=0 skips it even on a desktop.
case "${DOTFILES_VSCODE:-auto}" in
0) say "vscode: DOTFILES_VSCODE=0; skipping."; exit 0 ;;
1) ;;
*) if ! has_desktop; then
say "vscode: no graphical session detected; skipping (DOTFILES_VSCODE=1 to force)."
exit 0
fi ;;
esac
# Upstream keys its download URLs on x64/arm64 rather than amd64/arm64.
case "$(fetch_arch)" in
x86_64) VSCODE_ARCH=x64 ;;
aarch64) VSCODE_ARCH=arm64 ;;
*) say "vscode: unsupported arch $(fetch_arch)"; exit 1 ;;
esac
# --- install the app tree ----------------------------------------------------
# Verify it RUNS, not just that the tree exists: a Kasm redeploy can leave a
# present-but-broken directory behind (the same failure node/ and ansible/
# guard against).
#
# The root fallback is not optional. Run as root, `code --version` refuses --
# it prints "You are trying to start Visual Studio Code as a super user"
# instead of the version, and still exits 0 -- so a bare run-check never
# matches and would re-download 325MB on every single bootstrap, silently.
# That is the forced-install path (DOTFILES_VSCODE=1 in a container or on a
# server), so for root only, fall back to the recorded version stamp.
installed_version="$("$PREFIX/bin/code" --version 2>/dev/null | head -1 || true)"
if [ "$installed_version" = "$VSCODE_VERSION" ]; then
say "vscode: $VSCODE_VERSION already present at $PREFIX."
elif [ "$(id -u)" = "0" ] && have_version code "$VSCODE_VERSION"; then
say "vscode: $VSCODE_VERSION already present at $PREFIX (stamped; root cannot query --version)."
else
say "vscode: installing $VSCODE_VERSION into $PREFIX (this is a ~325MB download)..."
install_tarball_tree \
"https://update.code.visualstudio.com/$VSCODE_VERSION/linux-$VSCODE_ARCH/stable" \
"VSCode-linux-$VSCODE_ARCH" "$PREFIX"
# bin/code is upstream's CLI wrapper (`code .`, `code --install-extension`);
# the Electron binary at the tree root is what the menu entry launches.
mkdir -p "$BIN_DIR"
ln -sf "$PREFIX/bin/code" "$BIN_DIR/code"
mark_version code "$VSCODE_VERSION"
say "vscode: installed $VSCODE_VERSION and linked $BIN_DIR/code."
fi
# --- menu entry --------------------------------------------------------------
# Icon= is an absolute path rather than a theme name: the bundled icon is a
# single 1024x1024 PNG and hicolor tops out at 512x512, so there is no correct
# themed directory to drop it in. The spec allows an absolute path, and it needs
# no icon-cache rebuild to take effect.
mkdir -p "$DESKTOP_DIR"
cat >"$DESKTOP_DIR/code.desktop" <<DESKTOP
[Desktop Entry]
Type=Application
Name=Visual Studio Code
GenericName=Text Editor
Comment=Code Editing. Redefined.
Exec=$PREFIX/code %F
Icon=$PREFIX/resources/app/resources/linux/code.png
Terminal=false
StartupNotify=true
StartupWMClass=Code
Categories=TextEditor;Development;IDE;
MimeType=text/plain;inode/directory;
Keywords=vscode;editor;
Actions=new-window;
[Desktop Action new-window]
Name=New Empty Window
Exec=$PREFIX/code --new-window %F
Icon=$PREFIX/resources/app/resources/linux/code.png
DESKTOP
say "vscode: wrote $DESKTOP_DIR/code.desktop"
# Refresh the per-user menu caches so the entry appears without a re-login.
if command -v update-desktop-database >/dev/null 2>&1; then
update-desktop-database "$DESKTOP_DIR" 2>/dev/null || true
fi
if command -v kbuildsycoca5 >/dev/null 2>&1; then
kbuildsycoca5 --noincremental >/dev/null 2>&1 || true
fi
# --- keyring prompt suppression ----------------------------------------------
# WHY: on first launch Electron's safeStorage reaches for the OS keyring through
# libsecret. On a KDE session that wakes kwalletd, which pops an unlock/create
# dialog before the editor is usable -- and since kwallet-pam is not installed
# on these hosts, nothing unlocks the wallet at login, so it prompts every
# session. "basic" keeps VS Code's secrets in its own store and never reaches
# for the wallet at all. Verified with dbus-monitor: no org.kde.KWallet traffic
# on launch.
#
# TRADE-OFF: basic is weaker than a real keyring. Secrets VS Code stores (Git
# and GitHub auth tokens, extension credentials) end up obfuscated on disk
# rather than sealed behind a wallet password. To move to the wallet later,
# install kwallet-pam so it unlocks at login, then change the value to
# "kwallet5" and restart VS Code.
#
# The path comes from product.json's dataFolderName (".vscode"), NOT the
# user-data-dir: argv.json lives at ~/.vscode/argv.json and a copy written
# anywhere else is read by nothing and silently ignored.
if [ ! -e "$ARGV_JSON" ]; then
mkdir -p "$(dirname "$ARGV_JSON")"
cat >"$ARGV_JSON" <<'ARGV'
// Runtime arguments for VS Code. See dotfiles vscode/install.sh.
{
// Keep secrets in VS Code's own store rather than the OS keyring, so KDE's
// kwallet does not prompt for an unlock on every launch.
"password-store": "basic"
}
ARGV
say "vscode: wrote $ARGV_JSON (password-store=basic)"
elif grep -q '"password-store"' "$ARGV_JSON"; then
say "vscode: $ARGV_JSON already sets password-store; leaving it alone."
else
# Merge rather than overwrite: this file is JSON-with-comments that VS Code
# and the user both own, and it carries a crash-reporter-id that should
# survive. Insert the key after the opening brace and keep the rest byte for
# byte.
cp -p "$ARGV_JSON" "$ARGV_JSON.bak"
if awk '
BEGIN { done = 0 }
{
print
if (!done && $0 ~ /^[[:space:]]*\{[[:space:]]*$/) {
print "\t// Added by dotfiles vscode/install.sh: keep secrets in VS Code'"'"'s"
print "\t// own store so kwallet does not prompt for an unlock on every launch."
print "\t\"password-store\": \"basic\","
done = 1
}
}
END { exit(done ? 0 : 1) }
' "$ARGV_JSON.bak" >"$ARGV_JSON.new"; then
mv "$ARGV_JSON.new" "$ARGV_JSON"
say "vscode: set password-store=basic in $ARGV_JSON (backup: $ARGV_JSON.bak)"
else
rm -f "$ARGV_JSON.new"
say "vscode: WARNING: could not find the object opening in $ARGV_JSON."
say " Add '\"password-store\": \"basic\"' by hand to stop the keyring prompt."
fi
fi
# A system-wide VS Code (from Microsoft's rpm/deb repo) shadows this one in the
# menu and shows a duplicate entry. Flag it rather than touching it -- removing
# it needs the root this module deliberately avoids.
if [ -e /usr/share/applications/com.microsoft.VSCode.desktop ]; then
say ""
say "vscode: NOTE: a system-wide VS Code package is also installed."
say " You will see two menu entries until it is removed:"
say " sudo dnf remove code # or: sudo apt remove code"
say ""
fi
say "vscode: ready."
+38 -9
View File
@@ -69,9 +69,10 @@ _DOTFILES_CONF="$HOME/.config/dotfiles"
[ -f "$_DOTFILES_CONF/secrets.env" ] && source "$_DOTFILES_CONF/secrets.env"
# opr NAME read one field straight from the vault
# opc NAME [VALUE] create or update a field (prompts, hidden, when VALUE is
# omitted, which keeps the secret out of shell history),
# then refresh and reload this shell
# opc NAME=VALUE create or update a field, then refresh and reload this
# opc NAME shell; with no VALUE (NAME or NAME=) it prompts, hidden,
# which keeps the secret out of shell history.
# `opc NAME VALUE` also still works.
# opd NAME delete a field, refresh, and unset it in this shell
# opl [VAULT [ITEM]] show every field and its current value (default: your
# vault and item from op.env)
@@ -94,19 +95,47 @@ opr() {
opc() {
emulate -L zsh
local name=$1 value
_dotfiles_env_name "$name" opc || return
if (( $# >= 2 )); then
if [[ $1 == *=* ]]; then
# NAME=VALUE: split on the first '=' so values may contain '='
name=${1%%=*} value=${1#*=}
elif (( $# >= 2 )); then
value=$2
else
fi
_dotfiles_env_name "$name" opc || return
if [[ -z $value ]]; then
read -rs "value?$name: " && print
[[ -n $value ]] || { print -u2 "opc: empty value for $name; nothing changed"; return 1; }
fi
# [concealed] makes it a password field; an existing field of that name is updated.
_dotfiles_op sh -c 'op item edit "$OP_ITEM" --vault "$OP_VAULT" "$1[concealed]=$2" >/dev/null' \
opc "$name" "$value" && ops
# op can exit non-zero with "(404) Not Found" after an edit it did save (seen with
# op 2.34.1 and 2.39.0), so its exit code is ignored: the field is read back instead.
_dotfiles_op sh -c '
clean() { printf "%s" "${1:-(no output)}" | sed "s/^\[ERROR\] [0-9/]* [0-9:]* //"; }
ref="op://$OP_VAULT/$OP_ITEM/$1"
err=$(op item edit "$OP_ITEM" --vault "$OP_VAULT" "$1[concealed]=$2" 2>&1 >/dev/null)
if [ "$(op read "$ref" 2>/dev/null)" = "$2" ]; then
printf "opc: saved %s\n" "$ref" >&2
else
printf "opc: could not save %s\n op said: %s\n check vault, item and service-account write access with: opl\n" \
"$ref" "$(clean "$err")" >&2
exit 1
fi' opc "$name" "$value" && ops
}
opd() {
_dotfiles_env_name "$1" opd || return
_dotfiles_op sh -c 'op item edit "$OP_ITEM" --vault "$OP_VAULT" "$1[delete]" >/dev/null' opd "$1" \
# As in opc, op's exit code is unreliable here: check the item's field labels instead.
_dotfiles_op sh -c '
clean() { printf "%s" "${1:-(no output)}" | sed "s/^\[ERROR\] [0-9/]* [0-9:]* //"; }
ref="op://$OP_VAULT/$OP_ITEM/$1"
err=$(op item edit "$OP_ITEM" --vault "$OP_VAULT" "$1[delete]" 2>&1 >/dev/null)
if ! item=$(op item get "$OP_ITEM" --vault "$OP_VAULT" --format json 2>&1); then
printf "opd: could not re-read %s to confirm the delete\n op said: %s\n" "$ref" "$(clean "$item")" >&2
exit 1
elif printf "%s" "$item" | jq -r ".fields[].label" | grep -qxF "$1"; then
printf "opd: could not delete %s\n op said: %s\n" "$ref" "$(clean "$err")" >&2
exit 1
fi
printf "opd: deleted %s\n" "$ref" >&2' opd "$1" \
&& unset "$1" && ops
}
opl() {