From dafe9ea18520c72002b4c9db950168aeb084951b Mon Sep 17 00:00:00 2001 From: Bastien Chanot Date: Wed, 24 Jun 2026 18:00:52 +0200 Subject: [PATCH] fix(install): source dtach-router at login instead of executing it Executing dtach-router broke its return-based interactive guard and errored on /dev/tty in non-interactive login shells (bash -lc, cron, scp). It is now sourced via a guarded, idempotent ~/.profile block (case $- in *i*) ... . dtach-router) installed by wire_dtach_profile(), which migrates the old execute-based block. Also adds cc (create) and d (re-summon) aliases to bashrc-linux. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01CN1KSmsuLG6TxSeN5m8xvM --- .claude/memory/decisions.md | 11 ++++++++++ .claude/memory/journal.md | 9 +++++++++ .claude/memory/learnings.md | 11 ++++++++++ README.md | 6 +++--- bash/bashrc-linux | 8 ++++++++ install.sh | 40 +++++++++++++++++++++++++++---------- 6 files changed, 72 insertions(+), 13 deletions(-) diff --git a/.claude/memory/decisions.md b/.claude/memory/decisions.md index 9c621cd..a1d306c 100644 --- a/.claude/memory/decisions.md +++ b/.claude/memory/decisions.md @@ -40,3 +40,14 @@ deployed via `sudo install -D -m 0644` to `/etc/profile.d/` from `install_disk_w the apt-get Linux block (see LRN-005). Alt rejected: per-user append to `~/.bashrc` — wanted the warn for EVERY login account on the box, not just the installing user, so system-wide profile.d won. Known limit: login-shell scope only (non-login terminals miss it). Status: done. + +## BDR-007 — dtach resume menu wired login-scope via guarded SOURCE in ~/.profile +2026-06-24. Wired dtach session-resume into `~/.profile` (login scope = once per SSH) as a guarded SOURCE +`case $- in *i*) [ -x ~/.local/bin/dtach-router ] && . … ;;`, NOT `~/.bashrc` (every interactive shell → +menu pops on each tab/subshell). Matches "à la connexion SSH" intent. install.sh `wire_dtach_profile()` +idempotent: awk strips prior block (marker-delimited managed block `# >>> claude-dtach >>>` + legacy +`DT=$(dt ls)…fi` execute block) then re-appends marker block. cc/d aliases live in bashrc-linux (sourced by +.profile BEFORE the router runs → available). Alts rejected: (a) source from `.bashrc` (router's own header +suggests it) — fires too often for login-only intent; (b) keep execute + string-parse — broke the return-based +guard (LRN-006) + fragile parse. Supersedes the old execute+string-parse block. Status: done in repo; live +~/.profile re-migrated this session. diff --git a/.claude/memory/journal.md b/.claude/memory/journal.md index b9b5cc7..483dfde 100644 --- a/.claude/memory/journal.md +++ b/.claude/memory/journal.md @@ -32,3 +32,12 @@ Added etc/profile.d/disk-usage-warning.sh (POSIX sh, warns bold red when / or /h install_disk_warning() in install.sh: sudo install -D -m 0644 → /etc/profile.d, gated in apt block (Linux-only: df --output=pcent GNU-only + /etc/profile.d Debian convention). shellcheck + sh -n CLEAN, both code paths runtime-verified. README + CLAUDE.md synced. Not committed (master, user to confirm). + +## 2026-06-24 — dtach login wiring fix (source not execute) + cc/d aliases +Old ~/.profile block EXECUTED dtach-router + parsed "Aucune session dtach." → broken: executing breaks +the script's return-based interactive guard → falls through → fzf/`dt at >/dev/tty` errors `/dev/tty: No +such device` in every non-interactive login shell (repro'd live on each Bash init). Replaced with guarded +SOURCE `case $- in *i*) ... . dtach-router` via idempotent wire_dtach_profile() (awk strips legacy + +marker block, re-appends marker block). Added cc (create) / d (re-summon) aliases to bashrc-linux. +shellcheck + bash -n CLEAN; migration simulated on real .profile copy. LRN-006 + BDR-007. README synced. +Not committed; live ~/.profile not yet re-migrated. diff --git a/.claude/memory/learnings.md b/.claude/memory/learnings.md index b7820c0..b35619d 100644 --- a/.claude/memory/learnings.md +++ b/.claude/memory/learnings.md @@ -36,3 +36,14 @@ absent on macOS BSD df. Any install step deploying such a snippet system-wide mu `command -v apt-get` (Linux) block, never the OS-agnostic path. Deploy idempotently with `sudo install -D -m 0644 src /etc/profile.d/x.sh` (-D makes the dir, overwrite = re-runnable). Caveat: `/etc/profile.d/*.sh` runs for LOGIN shells only — non-login terminals need `/etc/bash.bashrc` instead. + +## LRN-006 — Login-resume scripts must be SOURCED, not executed +2026-06-24. `dtach-router` (any login script that hands control back via `return` + attaches to host TTY) +must be SOURCED, never run as a command. Executed: its guard `case $- in *i*) ;; *) return 0 2>/dev/null ;;` +can't `return` from a non-sourced script → error swallowed by `2>/dev/null` → falls THROUGH the guard → +runs fzf + `dt at … >/dev/tty` → `/dev/tty: No such device or address` in EVERY non-interactive login shell +(`bash -lc`, cron, scp, tool sandbox). Repro'd live (fired on each Bash init). Fix in `~/.profile`: +`case $- in *i*) [ -x router ] && . router ;; esac`. Also: don't re-guard by parsing decorative output +(`[ "$(dt ls)" != "Aucune session dtach." ]`) — fragile (couples to exact string) AND redundant +(`dtach-router` already returns on empty `dt --raw`). Let the script self-guard. Bonus gotcha: `~/.profile` +is NOT read by bash if `~/.bash_profile` or `~/.bash_login` exists. diff --git a/README.md b/README.md index 951424a..6c42637 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ curl -fsSL https://git.bchanot.fr/bchanot/config/raw/branch/master/remote-instal | `bash/bashrc-linux` | bashrc for desktop Linux (git-aware prompt + command timer). | | `bash/bashrc-osx` | bashrc for macOS. | | `bin/dt` | dtach session manager for claude-in-dtach sessions. | -| `bin/dtach-router` | SSH-login dashboard to resume dtach sessions (sourced from bashrc). | +| `bin/dtach-router` | SSH-login dashboard to resume dtach sessions (wired into `~/.profile` by the installer). | | `bin/claude-provider`| Switch Claude Code between Anthropic and OpenRouter. | | `etc/profile.d/disk-usage-warning.sh` | Login-time warning (bold red) when `/` or `/home` cross 85% usage. Deployed to `/etc/profile.d/` on Linux. | @@ -57,7 +57,7 @@ What it does: 5. Copies the tracked vim files into `~/.vim` and symlinks `~/.vimrc`. 6. Picks the bashrc by OS: macOS → `bashrc-osx` (falls back to `bashrc-linux` if missing), everything else → `bashrc-linux`. Copies it to `~/.bashrc`. 7. Installs Python CLIs via `pipx` (`PyMuPDF` → `pymupdf`, `Markdown` → `markdown_py`) — skipped if `pipx` is absent. -8. Copies the `bin/` scripts (`dt`, `dtach-router`, `claude-provider`) into `~/.local/bin`. +8. Copies the `bin/` scripts (`dt`, `dtach-router`, `claude-provider`) into `~/.local/bin` and wires the dtach session-resume menu into `~/.profile` (idempotent — sourced only at interactive login, and replaces any prior block). 9. On Linux, installs `etc/profile.d/disk-usage-warning.sh` to `/etc/profile.d/` (needs `sudo`) so each login warns when `/` or `/home` cross 85% usage. ### Packages installed (apt) @@ -79,7 +79,7 @@ The script is re-runnable: each run re-backs up to `~/Oldconfig` (overwriting th Deployed to `~/.local/bin` (the deployed bashrc adds this dir to `PATH`): - **`dt`** — manage claude-in-dtach sessions (`dt ls|at|kill`). Needs `dtach` + `fzf`. -- **`dtach-router`** — source from `~/.bashrc` to get a session dashboard on SSH login. Needs `dt`, `dtach`, `fzf`. +- **`dtach-router`** — session dashboard on SSH login. The installer wires it into `~/.profile`, where it is **sourced** (not executed) at interactive login and is a silent no-op when no session exists. Create a session with `cc [name]`, re-open the menu anytime with `d` (both aliases from the bashrc). Needs `dt`, `dtach`, `fzf`. - **`claude-provider`** — switch Claude Code between Anthropic and OpenRouter. OpenRouter mode reads the key from **`$OPENROUTER_API_KEY`** (never hardcoded). Export it from a private, untracked file, e.g. `~/.bashrc.local`: ```sh diff --git a/bash/bashrc-linux b/bash/bashrc-linux index ea4f1b9..78db31a 100644 --- a/bash/bashrc-linux +++ b/bash/bashrc-linux @@ -111,3 +111,11 @@ function set_prompt { trap 'timer_start' DEBUG PROMPT_COMMAND='set_prompt' ## Lancement des commandes au demarrages + +# claude-dans-dtach : creer une session (claude tournant dans dtach, detache via Ctrl-\). +# Usage : cd ~/projets/seo && cc seo -> session nommee "seo". +dtach_claude() { dtach -c "$HOME/.dtach/${1:-claude-$(date +%H%M%S)}" -e '^\' claude; } +alias cc='dtach_claude' + +# Rappeler a la demande le menu de reprise (sinon il s'affiche seul au login SSH). +alias d='source ~/.local/bin/dtach-router' diff --git a/install.sh b/install.sh index eade4c8..4c9b1c5 100755 --- a/install.sh +++ b/install.sh @@ -113,6 +113,34 @@ install_disk_warning() { /etc/profile.d/disk-usage-warning.sh } +# Wire the dtach session-resume menu into ~/.profile. At interactive login we SOURCE +# dtach-router rather than execute it: the script hands control back with `return` and +# attaches to the host TTY, so running it as a command makes its interactivity guard +# fail and errors on /dev/tty whenever the login shell is non-interactive (bash -lc, +# cron, scp). Sourced, the guard works and it is a silent no-op when no session exists. +# Idempotent: strips any prior block first — the marker-delimited managed one AND the +# legacy execute-based one (DT=$(dt ls) ... fi) from earlier installs. User scope, no sudo. +wire_dtach_profile() { + local profile="$HOME/.profile" + touch "$profile" + + awk ' + $0 == "# >>> claude-dtach >>>" { drop = 1; next } + $0 == "# <<< claude-dtach <<<" { drop = 0; next } + drop { next } + $0 == "DT=$(dt ls)", $0 == "fi" { next } + { print } + ' "$profile" > "$profile.tmp" && mv "$profile.tmp" "$profile" + + cat >> "$profile" << 'EOF' + +# >>> claude-dtach >>> +# At interactive login, offer to resume a claude-in-dtach session (no-op if none). +case $- in *i*) [ -x "$HOME/.local/bin/dtach-router" ] && . "$HOME/.local/bin/dtach-router" ;; esac +# <<< claude-dtach <<< +EOF +} + # System packages: Debian/Ubuntu only. Skipped where apt-get is absent (e.g. macOS). if command -v apt-get >/dev/null 2>&1; then sudo apt-get update @@ -196,16 +224,8 @@ cp "$SCRIPT_DIR"/bin/* "$HOME/.local/bin/" chmod +x "$HOME"/.local/bin/dt "$HOME"/.local/bin/dtach-router "$HOME"/.local/bin/claude-provider -# Append the dtach auto-router to ~/.profile once, so each login resumes sessions. -if ! grep -q "Aucune session dtach." "$HOME/.profile" 2>/dev/null; then - cat >> "$HOME/.profile" << 'EOF' - -DT=$(dt ls) -if [ "$DT" != "Aucune session dtach." ]; then - dtach-router -fi -EOF -fi +# Wire the dtach session-resume menu into ~/.profile (idempotent; migrates any prior block). +wire_dtach_profile echo "Done. Restart your shell or run: source ~/.bashrc" echo "If you use zsh, switch to bash to enjoy these settings =)"