workstation: machine-wide config inheritance (managed claudeMd + setup-devvm.sh + skel)
Spike confirmed (claude 2.1.168): /etc/claude-code/managed-settings.json claudeMd reaches a session (sentinel echoed). Hybrid inheritance = enforced org claudeMd machine-wide (top precedence, non-overridable) + per-user ~/.claude/{skills,rules,...} symlinks to the config base (live, the proven emo pattern) seeded via /etc/skel. setup-devvm.sh is idempotent: apt toolset, node>=18 + claude-code, system-wide kubelogin (NOT the Azure apt pkg), the managed config, and /etc/skel (launcher that cd's $HOME/code, tmux UX, inheritance symlinks). Verified: emo unchanged (groups/symlinks/live sessions intact), emo can read the managed config, idempotent re-run clean.
Security fix (host state): /home/wizard/.claude/settings.json was 0664, exposing MEMORY_API_KEY to all devvm users -> chmod 0600. chezmoi source needs a private_ prefix + the key templated out to persist this (dotfiles-repo follow-up).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
55d4b4cf2d
commit
1757cb59e7
5 changed files with 165 additions and 0 deletions
3
scripts/workstation/.gitignore
vendored
Normal file
3
scripts/workstation/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
__pycache__/
|
||||
.pytest_cache/
|
||||
*.pyc
|
||||
3
scripts/workstation/managed-settings.json
Normal file
3
scripts/workstation/managed-settings.json
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"claudeMd": "# Viktor Barzin homelab — shared multi-user Claude Code Workstation (devvm)\n\nYou are running as a specific OS user on a SHARED devvm Workstation, not as the admin. These org-wide rules apply to EVERY user and sit at the top of settings precedence (they cannot be overridden by a user's own config):\n\n- Respect your permission tier. Your kubectl, Vault, and infra access are scoped to your RBAC tier (admin / power-user / namespace-owner). Do not attempt to escalate privileges or reach another user's resources.\n- Secrets are per-user. Never read another user's home directory, credentials, tokens, or ~/.claude secrets. Your own secrets live in your home at mode 600.\n- Infrastructure changes go through Terraform/Terragrunt (scripts/tg apply) — never direct kubectl apply/edit/patch. Pushing to git does NOT deploy; applies are manual and admin-gated, so your edits cannot take effect without an admin apply.\n- Follow the engineering rules in ~/.claude/rules/ (execution, planning, quality) and every CLAUDE.md in the repo tree.\n- The monorepo is at ~/code. Non-admins get a git-crypt-LOCKED clone: secret files read as ciphertext — that is expected, not an error."
|
||||
}
|
||||
66
scripts/workstation/setup-devvm.sh
Executable file
66
scripts/workstation/setup-devvm.sh
Executable file
|
|
@ -0,0 +1,66 @@
|
|||
#!/usr/bin/env bash
|
||||
# Idempotent machine-wide host base for the devvm Claude Code Workstation.
|
||||
# Run as root. Sets up ONLY machine-wide state: the apt toolset, node + claude-code,
|
||||
# kubelogin, the ENFORCED managed Claude config, and /etc/skel defaults (launcher,
|
||||
# tmux UX, and live config-inheritance symlinks into the shared config base).
|
||||
#
|
||||
# PER-USER provisioning (accounts, per-tier groups, kubeconfig, secrets, infra
|
||||
# clone) lives in t3-provision-users.sh — NOT here. Safe to re-run.
|
||||
set -euo pipefail
|
||||
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
# The shared config base every user inherits from (live, chezmoi-versioned).
|
||||
# Coupled to the admin's home today; override to relocate to a neutral path.
|
||||
CONFIG_BASE="${WORKSTATION_CONFIG_BASE:-/home/wizard/.claude}"
|
||||
[[ $EUID -eq 0 ]] || { echo "setup-devvm.sh: must run as root" >&2; exit 1; }
|
||||
log() { echo "[setup-devvm] $*"; }
|
||||
|
||||
# 1) apt toolset (declarative manifest; comments/blank lines stripped)
|
||||
mapfile -t PKGS < <(grep -vE '^[[:space:]]*(#|$)' "$HERE/packages.txt")
|
||||
log "apt: ensuring ${#PKGS[@]} packages present"
|
||||
export DEBIAN_FRONTEND=noninteractive
|
||||
apt-get update -qq
|
||||
apt-get install -y "${PKGS[@]}" >/dev/null
|
||||
|
||||
# 2) node >= 18 + claude-code (claude-code requires node >= 18)
|
||||
need_node=1
|
||||
if command -v node >/dev/null; then
|
||||
[[ "$(node -v | sed 's/^v\([0-9]*\).*/\1/')" -ge 18 ]] && need_node=0
|
||||
fi
|
||||
if [[ $need_node -eq 1 ]]; then
|
||||
log "node: installing NodeSource 22.x"
|
||||
curl -fsSL https://deb.nodesource.com/setup_22.x | bash - >/dev/null
|
||||
apt-get install -y nodejs >/dev/null
|
||||
fi
|
||||
command -v claude >/dev/null || { log "npm: installing @anthropic-ai/claude-code"; npm install -g @anthropic-ai/claude-code >/dev/null; }
|
||||
|
||||
# 3) kubelogin (kubectl oidc-login) system-wide — NOT the apt 'kubelogin' (= Azure tool)
|
||||
if [[ ! -x /usr/local/bin/kubelogin ]]; then
|
||||
log "kubelogin: installing int128/kubelogin"
|
||||
tmp="$(mktemp -d)"
|
||||
curl -fsSL -o "$tmp/kl.zip" https://github.com/int128/kubelogin/releases/latest/download/kubelogin_linux_amd64.zip
|
||||
( cd "$tmp" && { unzip -o kl.zip kubelogin >/dev/null 2>&1 || python3 -m zipfile -e kl.zip .; } )
|
||||
install -m 0755 "$tmp/kubelogin" /usr/local/bin/kubelogin
|
||||
ln -sf /usr/local/bin/kubelogin /usr/local/bin/kubectl-oidc_login
|
||||
rm -rf "$tmp"
|
||||
fi
|
||||
|
||||
# 4) machine-wide ENFORCED Claude config (org claudeMd; top precedence; NO secrets)
|
||||
install -d -m 0755 /etc/claude-code
|
||||
install -m 0644 "$HERE/managed-settings.json" /etc/claude-code/managed-settings.json
|
||||
log "managed-settings.json -> /etc/claude-code/ (enforced org claudeMd)"
|
||||
|
||||
# 5) /etc/skel for NEW accounts: launcher + tmux UX + live-inheritance symlinks.
|
||||
# A symlink placed in /etc/skel is copied (as a symlink) into each new home by
|
||||
# `useradd -m`, so new users' ~/.claude/{skills,rules,...} resolve to the shared
|
||||
# base and pick up the admin's edits live. Secrets + hooks are per-user (written
|
||||
# by the provisioner), NEVER symlinked here.
|
||||
install -d -m 0755 /etc/skel
|
||||
install -m 0755 "$HERE/skel/start-claude.sh" /etc/skel/start-claude.sh
|
||||
install -m 0644 "$HERE/skel/tmux.conf" /etc/skel/.tmux.conf
|
||||
install -d -m 0755 /etc/skel/.claude
|
||||
for d in skills rules agents commands; do
|
||||
[[ -d "$CONFIG_BASE/$d" ]] && ln -sfn "$CONFIG_BASE/$d" "/etc/skel/.claude/$d"
|
||||
done
|
||||
log "skel: launcher + tmux + inheritance symlinks (base=$CONFIG_BASE)"
|
||||
|
||||
log "OK (idempotent)"
|
||||
42
scripts/workstation/skel/start-claude.sh
Executable file
42
scripts/workstation/skel/start-claude.sh
Executable file
|
|
@ -0,0 +1,42 @@
|
|||
#!/bin/bash
|
||||
# Per-user Claude Code Workstation launcher (devvm). Lands the user in their OWN
|
||||
# ~/code clone (NOT a hardcoded /home/wizard/code) and names the Claude session
|
||||
# after the tmux session so /resume, the prompt box, and the terminal title line
|
||||
# up. Deployed via /etc/skel by setup-devvm.sh, so new accounts get it on
|
||||
# `useradd -m`. Existing users are repointed to this during their migration.
|
||||
echo ""
|
||||
echo " Welcome, $(id -un)! 🚀"
|
||||
echo ""
|
||||
echo " Starting Claude Code in $HOME/code ..."
|
||||
echo " (Right-click for tmux menu, or Ctrl+B then | or - to split)"
|
||||
echo ""
|
||||
|
||||
name_args=()
|
||||
if [ -n "${TMUX:-}" ]; then
|
||||
sess="$(tmux display-message -p '#{session_name}' 2>/dev/null)"
|
||||
[ -n "$sess" ] && name_args=(--name "$sess")
|
||||
fi
|
||||
|
||||
cd "$HOME/code" 2>/dev/null || cd "$HOME"
|
||||
|
||||
# Prefer the system-wide `claude` (installed by setup-devvm.sh); fall back to npx.
|
||||
launch() {
|
||||
if command -v claude >/dev/null 2>&1; then
|
||||
claude "$@"
|
||||
else
|
||||
npx @anthropic-ai/claude-code "$@"
|
||||
fi
|
||||
}
|
||||
|
||||
# Deliberately not `exec` so we can branch on the exit code: clean quit ends the
|
||||
# pane (ttyd closes the terminal); a crash drops to a shell so the tmux session
|
||||
# isn't destroyed-and-recreated in a ttyd auto-reconnect loop.
|
||||
launch --dangerously-skip-permissions --model claude-opus-4-8 "${name_args[@]}"
|
||||
code=$?
|
||||
[ "$code" -eq 0 ] && exit 0
|
||||
|
||||
echo ""
|
||||
echo " claude exited abnormally (status $code). Dropping to a shell — your tmux session is preserved."
|
||||
echo " Re-launch any time with: ~/start-claude.sh"
|
||||
echo ""
|
||||
exec "${SHELL:-/bin/bash}" -l
|
||||
51
scripts/workstation/skel/tmux.conf
Normal file
51
scripts/workstation/skel/tmux.conf
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
# Workstation base tmux config (deployed to /etc/skel/.tmux.conf by
|
||||
# setup-devvm.sh; new accounts inherit it). Uses $HOME (expanded by the shell at
|
||||
# run time) so it works for ANY user — never a hardcoded /home/<name>.
|
||||
#
|
||||
# NOTE: the tmux-resurrect/continuum "persistence" block is owned by the separate
|
||||
# terminal-lobby tool, which appends its own managed section + installs tpm. This
|
||||
# base file intentionally omits it so a fresh account isn't left with broken
|
||||
# `run ~/.tmux/plugins/tpm/tpm` references before terminal-lobby runs.
|
||||
|
||||
# Launch the per-user Claude launcher in every new pane/window (lands in ~/code).
|
||||
set -g default-command "$HOME/start-claude.sh"
|
||||
|
||||
# Mouse support — click panes, drag to resize, scroll with wheel
|
||||
set -g mouse on
|
||||
|
||||
# Easy splits: Ctrl+b then | for vertical, - for horizontal
|
||||
bind | split-window -h -c "#{pane_current_path}"
|
||||
bind - split-window -v -c "#{pane_current_path}"
|
||||
bind c new-window -c "#{pane_current_path}"
|
||||
|
||||
# Right-click context menu — clickable actions popup
|
||||
bind -n MouseDown3Pane display-menu -T "#[align=centre]Terminal Menu" -x M -y M \
|
||||
"New Claude" w "new-window -c '#{pane_current_path}'" \
|
||||
"Split Horizontal" h "split-window -v -c '#{pane_current_path}'" \
|
||||
"Split Vertical" v "split-window -h -c '#{pane_current_path}'" \
|
||||
"" \
|
||||
"Shell" s "split-window -v -c '#{pane_current_path}' /bin/zsh" \
|
||||
"" \
|
||||
"Close Pane" x "confirm-before -p 'Close pane? (y/n)' kill-pane" \
|
||||
"Close Window" X "confirm-before -p 'Close window? (y/n)' kill-window" \
|
||||
"" \
|
||||
"Detach" d "detach-client"
|
||||
|
||||
# Clickable [+] button in the status bar — left-click to open the same menu
|
||||
set -g status-right '#[fg=black bg=green] [+] #[default] #[fg=cyan]Right-click for menu '
|
||||
set -g status-right-length 60
|
||||
bind -n MouseDown1StatusRight display-menu -T "#[align=centre]Terminal Menu" -x M -y S \
|
||||
"New Claude" w "new-window -c '#{pane_current_path}'" \
|
||||
"Split Horizontal" h "split-window -v -c '#{pane_current_path}'" \
|
||||
"Split Vertical" v "split-window -h -c '#{pane_current_path}'" \
|
||||
"" \
|
||||
"Shell" s "split-window -v -c '#{pane_current_path}' /bin/zsh" \
|
||||
"" \
|
||||
"Close Pane" x "confirm-before -p 'Close pane? (y/n)' kill-pane" \
|
||||
"Close Window" X "confirm-before -p 'Close window? (y/n)' kill-window"
|
||||
|
||||
# Status bar styling + 1-based numbering
|
||||
set -g status-style 'bg=colour235 fg=colour136'
|
||||
set -g status-left '#[fg=green][#S] '
|
||||
set -g base-index 1
|
||||
setw -g pane-base-index 1
|
||||
Loading…
Add table
Add a link
Reference in a new issue