From 25065ef38f1287d87477a62eb6599c6157fc2eb4 Mon Sep 17 00:00:00 2001 From: Joe McKenney Date: Sun, 1 Feb 2026 09:44:09 -0800 Subject: [PATCH 1/2] test: reproduction of issue 10 --- scripts/repro-issue-10.sh | 82 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100755 scripts/repro-issue-10.sh diff --git a/scripts/repro-issue-10.sh b/scripts/repro-issue-10.sh new file mode 100755 index 0000000..0b37733 --- /dev/null +++ b/scripts/repro-issue-10.sh @@ -0,0 +1,82 @@ +#!/bin/bash +# Reproduction script for https://github.com/joemckenney/wake/issues/10 +# Issue: No commands recorded on immutable Linux OS (Fedora Atomic) +# +# Root cause: SHELL env var is not exported on Fedora, so wake falls back +# to /bin/sh which doesn't source .bashrc, so hooks never load. + +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(dirname "$SCRIPT_DIR")" + +# Create Dockerfile +DOCKERFILE=$(mktemp) +cat > "$DOCKERFILE" << 'DOCKERFILE_CONTENT' +FROM fedora:43 + +# Install utilities +RUN dnf install -y util-linux-script which && dnf clean all + +# Mimic Fedora Atomic home directory structure +RUN mkdir -p /var/home/testuser && \ + rm -rf /home && \ + ln -s /var/home /home && \ + useradd -d /var/home/testuser -s /bin/bash testuser && \ + chown -R testuser:testuser /var/home/testuser + +# Install wake binary +COPY target/release/wake /usr/local/bin/wake +RUN chmod +x /usr/local/bin/wake + +# Set up bashrc with wake hooks +USER testuser +WORKDIR /var/home/testuser +RUN echo 'eval "$(wake init bash)"' >> ~/.bashrc + +CMD ["/bin/bash"] +DOCKERFILE_CONTENT + +echo "=== Building container ===" +docker build -f "$DOCKERFILE" -t wake-fedora-test "$REPO_ROOT" + +echo "" +echo "=== Test 1: Confirm SHELL is not exported ===" +docker run --rm wake-fedora-test bash -c ' + echo "SHELL variable: $SHELL" + echo "SHELL in env: $(printenv SHELL || echo "NOT EXPORTED")" +' + +echo "" +echo "=== Test 2: Reproduce the issue ===" +docker run --rm wake-fedora-test script -q -c ' + wake shell << INNER + echo "Running inside wake shell..." + echo "Hook defined: \$(type __wake_preexec 2>/dev/null && echo YES || echo NO)" + ls /tmp > /dev/null + exit +INNER + echo "" + echo "Commands recorded:" + wake log -c 3 +' /dev/null 2>&1 | strings + +echo "" +echo "=== Test 3: Confirm workaround (export SHELL) ===" +docker run --rm wake-fedora-test script -q -c ' + export SHELL # THE FIX + wake shell << INNER + echo "Hook defined: \$(type __wake_preexec 2>/dev/null && echo YES || echo NO)" + ls /tmp > /dev/null + exit +INNER + echo "" + echo "Commands recorded:" + wake log -c 3 +' /dev/null 2>&1 | strings + +# Cleanup +rm -f "$DOCKERFILE" + +echo "" +echo "=== Done ===" From f6723bd21eef1be8c3888e497de636d9523993eb Mon Sep 17 00:00:00 2001 From: Joe McKenney Date: Sun, 1 Feb 2026 09:49:51 -0800 Subject: [PATCH 2/2] fix: read user shell from /etc/passwd when SHELL env var not set On some systems (notably Fedora Atomic variants), the SHELL environment variable is set as a shell variable but not exported to the environment. This caused wake to fall back to /bin/sh, which doesn't source .bashrc, so the shell hooks never loaded and no commands were recorded. Now wake tries multiple sources for the user's shell: 1. SHELL environment variable (existing behavior) 2. /etc/passwd entry for current user (new fallback) 3. /bin/sh (final fallback) Fixes #10 Co-Authored-By: Claude Opus 4.5 --- crates/wake-cli/src/commands/shell.rs | 110 +++++++++++++++++++++++++- 1 file changed, 107 insertions(+), 3 deletions(-) diff --git a/crates/wake-cli/src/commands/shell.rs b/crates/wake-cli/src/commands/shell.rs index 89ac276..4f07a1a 100644 --- a/crates/wake-cli/src/commands/shell.rs +++ b/crates/wake-cli/src/commands/shell.rs @@ -1,12 +1,55 @@ use anyhow::{Context, Result}; use chrono::Utc; use portable_pty::{native_pty_system, CommandBuilder, PtySize}; -use std::io::{Read, Write}; +use std::io::{BufRead, BufReader, Read, Write}; use std::os::unix::net::UnixListener; use std::sync::{Arc, Mutex}; use tokio::sync::mpsc; use wake_core::{Config, Database, GitCache, HookMessage, OutputBuffer, SummarizationConfig}; +/// Get the user's shell, trying multiple sources: +/// 1. SHELL environment variable +/// 2. /etc/passwd entry for current user +/// 3. Fall back to /bin/sh +fn get_user_shell() -> String { + // Try SHELL env var first + if let Ok(shell) = std::env::var("SHELL") { + return shell; + } + + // Try to read from /etc/passwd + if let Some(shell) = get_shell_from_passwd() { + return shell; + } + + // Final fallback + "/bin/sh".to_string() +} + +/// Read the current user's shell from /etc/passwd +fn get_shell_from_passwd() -> Option { + let uid = unsafe { libc::getuid() }; + let file = std::fs::File::open("/etc/passwd").ok()?; + let reader = BufReader::new(file); + + for line in reader.lines().map_while(Result::ok) { + // Format: username:password:uid:gid:gecos:home:shell + let fields: Vec<&str> = line.split(':').collect(); + if fields.len() >= 7 { + if let Ok(entry_uid) = fields[2].parse::() { + if entry_uid == uid { + let shell = fields[6].trim(); + if !shell.is_empty() { + return Some(shell.to_string()); + } + } + } + } + } + + None +} + struct PendingCommand { id: i64, command: String, @@ -38,8 +81,8 @@ pub async fn run() -> Result<()> { // Clean up any stale socket let _ = std::fs::remove_file(&socket_path); - // Get user's shell - let shell = std::env::var("SHELL").unwrap_or_else(|_| "/bin/sh".to_string()); + // Get user's shell (env var -> /etc/passwd -> /bin/sh fallback) + let shell = get_user_shell(); // Detect project root let cwd = std::env::current_dir().ok(); @@ -397,3 +440,64 @@ impl Drop for RawModeGuard { } } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_get_shell_from_passwd_returns_valid_shell() { + // This test runs as the current user, so it should find a shell + let shell = get_shell_from_passwd(); + assert!(shell.is_some(), "Should find current user's shell in /etc/passwd"); + let shell = shell.unwrap(); + assert!(shell.starts_with('/'), "Shell should be an absolute path"); + assert!( + shell.contains("sh") || shell.contains("zsh") || shell.contains("fish"), + "Shell should be a known shell type: {}", + shell + ); + } + + #[test] + fn test_get_user_shell_returns_shell() { + let shell = get_user_shell(); + assert!(shell.starts_with('/'), "Shell should be an absolute path"); + assert!(!shell.is_empty(), "Shell should not be empty"); + } + + #[test] + fn test_get_user_shell_prefers_env_var() { + // Save original + let original = std::env::var("SHELL").ok(); + + // Set a custom SHELL + std::env::set_var("SHELL", "/bin/custom-shell"); + let shell = get_user_shell(); + assert_eq!(shell, "/bin/custom-shell"); + + // Restore original + match original { + Some(val) => std::env::set_var("SHELL", val), + None => std::env::remove_var("SHELL"), + } + } + + #[test] + fn test_get_user_shell_falls_back_to_passwd() { + // Save original + let original = std::env::var("SHELL").ok(); + + // Remove SHELL env var + std::env::remove_var("SHELL"); + + let shell = get_user_shell(); + // Should get shell from passwd, not /bin/sh (unless that's actually the user's shell) + assert!(shell.starts_with('/'), "Shell should be an absolute path"); + + // Restore original + if let Some(val) = original { + std::env::set_var("SHELL", val); + } + } +}