| Server IP : 202.61.199.114 / Your IP : 216.73.217.139 Web Server : nginx/1.22.1 System : Linux de.arni-solutions.de 6.1.0-49-amd64 #1 SMP PREEMPT_DYNAMIC Debian 6.1.174-1 (2026-05-26) x86_64 User : web20 ( 1018) PHP Version : 8.4.23 Disable Function : NONE MySQL : OFF | cURL : ON | WGET : ON | Perl : ON | Python : OFF | Sudo : ON | Pkexec : ON Directory : /home/scripts/ |
Upload File : |
#!/usr/bin/env bash
#
# claude-session-manager.sh
# ---------------------------------------------------------------------------
# Verwaltung von Claude-Code-Sessions je ISPConfig-Web (Debian / ISPConfig 3)
#
# - liest verfuegbare Webs direkt aus der ISPConfig-Datenbank (dbispconfig)
# - Auswahl der Domain ueber ein Menue
# - passt alle Befehle automatisch an den gewaehlten Web-User an
# - zeigt jeden Befehl vor der Ausfuehrung an (Preview + Bestaetigung)
# - einzelne Schritte frei im Menue waehlbar
#
# Autor: erstellt fuer Nico (SeaSide Travel / ISPConfig-Stack)
# ---------------------------------------------------------------------------
set -o pipefail
# --------------------------------------------------------------------------- #
# Konfiguration
# --------------------------------------------------------------------------- #
# Schema fuer den tmux-Session-Namen:
# "domain" -> claude-taschenmuschi-kaufen-de (Punkte werden zu - )
# "user" -> claude-web32
SESSION_SCHEME="domain"
# --------------------------------------------------------------------------- #
# Farben / Stil
# --------------------------------------------------------------------------- #
if [[ -t 1 ]]; then
C_RESET=$'\e[0m'; C_BOLD=$'\e[1m'; C_DIM=$'\e[2m'
C_RED=$'\e[38;5;203m'; C_GRN=$'\e[38;5;114m'; C_YLW=$'\e[38;5;221m'
C_BLU=$'\e[38;5;75m'; C_CYN=$'\e[38;5;80m'; C_MAG=$'\e[38;5;176m'
C_GRY=$'\e[38;5;245m'
else
C_RESET=""; C_BOLD=""; C_DIM=""; C_RED=""; C_GRN=""; C_YLW=""
C_BLU=""; C_CYN=""; C_MAG=""; C_GRY=""
fi
BAR="${C_BLU}────────────────────────────────────────────────────────────────${C_RESET}"
# --------------------------------------------------------------------------- #
# Hilfsausgaben
# --------------------------------------------------------------------------- #
ok() { echo -e " ${C_GRN}✔${C_RESET} $*"; }
err() { echo -e " ${C_RED}✘${C_RESET} $*"; }
warn() { echo -e " ${C_YLW}!${C_RESET} $*"; }
info() { echo -e " ${C_CYN}»${C_RESET} $*"; }
pause() { echo; read -rp " ${C_DIM}[Enter] um fortzufahren …${C_RESET} " _; }
confirm() {
local q="${1:-Befehl jetzt ausfuehren?}"
echo
read -rp " ${C_YLW}?${C_RESET} ${q} ${C_DIM}[j/N]${C_RESET} " a
[[ "$a" =~ ^([jJyY])$ ]]
}
# Zeigt einen Befehl als Preview-Block (mehrzeilig, lesbar) ------------------ #
preview() {
echo
echo -e " ${C_GRY}┌─ auszufuehrender Befehl ───────────────────────────────${C_RESET}"
while IFS= read -r line; do
echo -e " ${C_GRY}│${C_RESET} ${C_MAG}${line}${C_RESET}"
done <<< "$1"
echo -e " ${C_GRY}└────────────────────────────────────────────────────────${C_RESET}"
}
# --------------------------------------------------------------------------- #
# Voraussetzungen
# --------------------------------------------------------------------------- #
require_root() {
if [[ $EUID -ne 0 ]]; then
err "Bitte als ${C_BOLD}root${C_RESET} starten (chown / sudo -u benoetigt root)."
exit 1
fi
}
check_deps() {
local missing=()
for bin in tmux sudo mysql; do
command -v "$bin" >/dev/null 2>&1 || missing+=("$bin")
done
if ((${#missing[@]})); then
err "Fehlende Programme: ${missing[*]}"
info "Installation: ${C_BOLD}apt install ${missing[*]}${C_RESET}"
exit 1
fi
}
# --------------------------------------------------------------------------- #
# ISPConfig-DB-Zugang aus config.inc.php lesen
# --------------------------------------------------------------------------- #
DB_HOST=""; DB_PORT="3306"; DB_USER=""; DB_PASS=""; DB_NAME="dbispconfig"
php_conf() { # $1 = datei $2 = key
grep -oP "\\\$conf\['$2'\]\s*=\s*'\K[^']*" "$1" | head -n1
}
read_db_config() {
local f
for f in /usr/local/ispconfig/server/lib/config.inc.php \
/usr/local/ispconfig/interface/lib/config.inc.php; do
[[ -f "$f" ]] || continue
DB_HOST=$(php_conf "$f" db_host)
DB_PORT=$(php_conf "$f" db_port)
DB_USER=$(php_conf "$f" db_user)
DB_PASS=$(php_conf "$f" db_password)
DB_NAME=$(php_conf "$f" db_database)
[[ -n "$DB_USER" ]] && break
done
[[ -z "$DB_HOST" ]] && DB_HOST="localhost"
[[ -z "$DB_PORT" ]] && DB_PORT="3306"
[[ -z "$DB_NAME" ]] && DB_NAME="dbispconfig"
[[ -n "$DB_USER" ]]
}
# MySQL-Aufruf ueber temporaere, geschuetzte Optionsdatei (Passwort nicht in ps)
mysql_q() {
local cnf rc
cnf=$(mktemp); chmod 600 "$cnf"
cat > "$cnf" <<EOF
[client]
host=${DB_HOST}
port=${DB_PORT}
user=${DB_USER}
password=${DB_PASS}
EOF
mysql --defaults-extra-file="$cnf" -N -B -e "$1"
rc=$?
rm -f "$cnf"
return $rc
}
# --------------------------------------------------------------------------- #
# Webs laden (Arrays: W_DOMAIN W_USER W_GROUP W_DOCROOT)
# --------------------------------------------------------------------------- #
declare -a W_DOMAIN W_USER W_GROUP W_DOCROOT
load_webs() {
W_DOMAIN=(); W_USER=(); W_GROUP=(); W_DOCROOT=()
local rows
if read_db_config; then
rows=$(mysql_q "SELECT domain, system_user, system_group, document_root
FROM ${DB_NAME}.web_domain
WHERE active='y'
AND system_user <> ''
AND type IN ('vhost','vhostsubdomain')
ORDER BY domain;")
if [[ -n "$rows" ]]; then
while IFS=$'\t' read -r dom usr grp doc; do
[[ -z "$dom" ]] && continue
W_DOMAIN+=("$dom"); W_USER+=("$usr")
W_GROUP+=("$grp"); W_DOCROOT+=("$doc")
done <<< "$rows"
return 0
fi
warn "Keine Eintraege in der ISPConfig-DB gefunden – nutze Fallback."
else
warn "ISPConfig-DB-Zugang nicht lesbar – nutze Fallback (/var/www)."
fi
# Fallback: /var/www-Symlinks + Owner aus dem Dateisystem
local link target usr grp
for link in /var/www/*; do
[[ -L "$link" ]] || continue
target=$(readlink -f "$link")
[[ -d "$target" ]] || continue
usr=$(stat -c '%U' "$target" 2>/dev/null)
grp=$(stat -c '%G' "$target" 2>/dev/null)
[[ "$usr" =~ ^web[0-9]+$ ]] || continue
W_DOMAIN+=("$(basename "$link")"); W_USER+=("$usr")
W_GROUP+=("$grp"); W_DOCROOT+=("$target")
done
((${#W_DOMAIN[@]} > 0))
}
# --------------------------------------------------------------------------- #
# Aktuelle Auswahl
# --------------------------------------------------------------------------- #
SEL_DOMAIN=""; SEL_USER=""; SEL_GROUP=""; SEL_DOCROOT=""
WORKDIR=""; CLAUDE_HOME=""; SESSION=""
resolve_paths() {
CLAUDE_HOME="/home/claude-${SEL_USER}"
# tmux-sichere Bezeichnung: Punkte/Doppelpunkte/Slashes -> Bindestrich
local safe_domain="${SEL_DOMAIN//[.:\/ ]/-}"
if [[ "$SESSION_SCHEME" == "domain" ]]; then
SESSION="claude-${safe_domain}"
else
SESSION="claude-${SEL_USER}"
fi
# bevorzugt der vom Nutzer genutzte Domain-Pfad, sonst document_root
if [[ -d "/var/www/${SEL_DOMAIN}/web" ]]; then WORKDIR="/var/www/${SEL_DOMAIN}/web"
elif [[ -d "${SEL_DOCROOT}/web" ]]; then WORKDIR="${SEL_DOCROOT}/web"
else WORKDIR="${SEL_DOCROOT}"
fi
}
session_running() {
sudo -u "$SEL_USER" env HOME="$CLAUDE_HOME" \
tmux has-session -t "$SESSION" 2>/dev/null
}
select_domain() {
if ! load_webs; then
err "Es konnten keine Webs ermittelt werden."
pause; return 1
fi
while true; do
clear; header_min
echo -e " ${C_BOLD}Verfuegbare Webs${C_RESET} ${C_DIM}(Quelle: ${DB_NAME:-Dateisystem})${C_RESET}"
echo -e " $BAR"
local i
for i in "${!W_DOMAIN[@]}"; do
printf " ${C_CYN}%2d${C_RESET}) ${C_BOLD}%-32s${C_RESET} ${C_GRY}%-8s %s${C_RESET}\n" \
$((i+1)) "${W_DOMAIN[$i]}" "${W_USER[$i]}" "${W_GROUP[$i]}"
done
echo -e " $BAR"
echo -e " ${C_DIM}0) zurueck${C_RESET}"
echo
read -rp " Domain waehlen: " n
[[ "$n" == "0" ]] && return 1
if [[ "$n" =~ ^[0-9]+$ ]] && ((n>=1 && n<=${#W_DOMAIN[@]})); then
local idx=$((n-1))
SEL_DOMAIN="${W_DOMAIN[$idx]}"; SEL_USER="${W_USER[$idx]}"
SEL_GROUP="${W_GROUP[$idx]}"; SEL_DOCROOT="${W_DOCROOT[$idx]}"
resolve_paths
return 0
fi
warn "Ungueltige Eingabe."; sleep 1
done
}
# --------------------------------------------------------------------------- #
# Schritte
# --------------------------------------------------------------------------- #
step_create_folder() {
clear; header
echo -e " ${C_BOLD}Schritt 1 – Claude-Home anlegen${C_RESET}\n $BAR"
info "Verzeichnis : ${C_BOLD}${CLAUDE_HOME}${C_RESET}"
info "Besitzer : ${C_BOLD}${SEL_USER}:${SEL_GROUP}${C_RESET} (chmod 700)"
preview "mkdir -p ${CLAUDE_HOME}
chown ${SEL_USER}:${SEL_GROUP} ${CLAUDE_HOME}
chmod 700 ${CLAUDE_HOME}"
if [[ -d "$CLAUDE_HOME" ]]; then
warn "Verzeichnis existiert bereits – Rechte werden nur aktualisiert."
fi
confirm || { info "Abgebrochen."; pause; return; }
if mkdir -p "$CLAUDE_HOME" \
&& chown "${SEL_USER}:${SEL_GROUP}" "$CLAUDE_HOME" \
&& chmod 700 "$CLAUDE_HOME"; then
ok "Claude-Home eingerichtet: ${CLAUDE_HOME}"
else
err "Fehler beim Anlegen / Setzen der Rechte."
fi
pause
}
step_start_session() {
clear; header
echo -e " ${C_BOLD}Schritt 2 – Claude-Session starten${C_RESET}\n $BAR"
if [[ ! -d "$CLAUDE_HOME" ]]; then
warn "Claude-Home ${CLAUDE_HOME} fehlt – bitte zuerst Schritt 1."
pause; return
fi
if [[ ! -d "$WORKDIR" ]]; then
err "Arbeitsverzeichnis ${WORKDIR} existiert nicht."
pause; return
fi
if session_running; then
warn "Session ${C_BOLD}${SESSION}${C_RESET} laeuft bereits."
pause; return
fi
info "Session : ${C_BOLD}${SESSION}${C_RESET}"
info "Workdir : ${C_BOLD}${WORKDIR}${C_RESET}"
preview "sudo -u ${SEL_USER} env HOME=${CLAUDE_HOME} USER=${SEL_USER} LOGNAME=${SEL_USER} SHELL=/bin/bash \\
tmux new -d -s ${SESSION} \\
-c ${WORKDIR} \\
'claude --dangerously-skip-permissions --remote-control'"
confirm || { info "Abgebrochen."; pause; return; }
if sudo -u "$SEL_USER" env HOME="$CLAUDE_HOME" USER="$SEL_USER" \
LOGNAME="$SEL_USER" SHELL=/bin/bash \
tmux new -d -s "$SESSION" -c "$WORKDIR" \
'claude --dangerously-skip-permissions --remote-control'; then
ok "Session ${SESSION} gestartet (detached)."
else
err "Start fehlgeschlagen – ist 'claude' fuer ${SEL_USER} im PATH?"
fi
pause
}
step_attach() {
clear; header
echo -e " ${C_BOLD}Schritt 3 – Session oeffnen (attach)${C_RESET}\n $BAR"
if ! session_running; then
warn "Keine laufende Session ${C_BOLD}${SESSION}${C_RESET}."
pause; return
fi
info "Loesen ohne Beenden: ${C_BOLD}Strg-b d${C_RESET}"
preview "sudo -u ${SEL_USER} env HOME=${CLAUDE_HOME} tmux attach -t ${SESSION}"
confirm "Jetzt verbinden?" || { info "Abgebrochen."; pause; return; }
sudo -u "$SEL_USER" env HOME="$CLAUDE_HOME" tmux attach -t "$SESSION"
}
step_list() {
clear; header
echo -e " ${C_BOLD}Schritt 4 – Laufende Sessions (User ${SEL_USER})${C_RESET}\n $BAR"
preview "sudo -u ${SEL_USER} env HOME=${CLAUDE_HOME} tmux ls"
echo
local out
out=$(sudo -u "$SEL_USER" env HOME="$CLAUDE_HOME" tmux ls 2>&1)
if [[ -z "$out" || "$out" == *"no server running"* ]]; then
info "Keine aktiven Sessions fuer ${SEL_USER}."
else
echo "$out" | sed 's/^/ /'
fi
pause
}
step_kill() {
clear; header
echo -e " ${C_BOLD}Session beenden${C_RESET}\n $BAR"
if ! session_running; then
warn "Keine laufende Session ${C_BOLD}${SESSION}${C_RESET}."
pause; return
fi
preview "sudo -u ${SEL_USER} env HOME=${CLAUDE_HOME} tmux kill-session -t ${SESSION}"
confirm "Session ${SESSION} wirklich beenden?" || { info "Abgebrochen."; pause; return; }
if sudo -u "$SEL_USER" env HOME="$CLAUDE_HOME" tmux kill-session -t "$SESSION"; then
ok "Session ${SESSION} beendet."
else
err "Konnte Session nicht beenden."
fi
pause
}
# --------------------------------------------------------------------------- #
# Header / Menue
# --------------------------------------------------------------------------- #
header_min() {
echo -e "${C_BLU}${C_BOLD}"
echo " ╔══════════════════════════════════════════════════════════════╗"
echo " ║ Claude Session Manager · ISPConfig / Debian ║"
echo " ╚══════════════════════════════════════════════════════════════╝"
echo -e "${C_RESET}"
}
header() {
header_min
local state
if session_running; then state="${C_GRN}● laeuft${C_RESET}"
else state="${C_GRY}○ gestoppt${C_RESET}"; fi
echo -e " ${C_DIM}Domain ${C_RESET} ${C_BOLD}${SEL_DOMAIN}${C_RESET}"
echo -e " ${C_DIM}Web ${C_RESET} ${SEL_USER}:${SEL_GROUP} ${C_DIM}Session${C_RESET} ${SESSION} [ ${state} ]"
echo -e " ${C_DIM}Workdir${C_RESET} ${WORKDIR}"
echo -e " ${C_DIM}Home ${C_RESET} ${CLAUDE_HOME}"
echo -e " $BAR"
}
main_menu() {
while true; do
clear; header
echo -e " ${C_CYN}1${C_RESET}) Claude-Home anlegen ${C_DIM}(mkdir + chown + chmod)${C_RESET}"
echo -e " ${C_CYN}2${C_RESET}) Session starten ${C_DIM}(tmux new, detached)${C_RESET}"
echo -e " ${C_CYN}3${C_RESET}) Session oeffnen ${C_DIM}(tmux attach)${C_RESET}"
echo -e " ${C_CYN}4${C_RESET}) Sessions anzeigen ${C_DIM}(tmux ls)${C_RESET}"
echo -e " ${C_CYN}5${C_RESET}) Session beenden ${C_DIM}(tmux kill-session)${C_RESET}"
echo -e " $BAR"
echo -e " ${C_YLW}d${C_RESET}) Domain wechseln"
echo -e " ${C_RED}q${C_RESET}) Beenden"
echo
read -rp " Auswahl: " c
case "$c" in
1) step_create_folder ;;
2) step_start_session ;;
3) step_attach ;;
4) step_list ;;
5) step_kill ;;
d|D) select_domain || true ;;
q|Q) clear; ok "Beendet."; exit 0 ;;
*) warn "Ungueltige Auswahl."; sleep 1 ;;
esac
done
}
# --------------------------------------------------------------------------- #
# Start
# --------------------------------------------------------------------------- #
require_root
check_deps
clear; header_min
info "Lese verfuegbare Webs aus ISPConfig …"
if ! select_domain; then
clear; err "Keine Domain gewaehlt – Abbruch."
exit 1
fi
main_menu