From 382eaff8899ca8f6c57b58490c6ab0480f3f2a77 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 21 Jan 2026 20:38:26 +0000 Subject: [PATCH 1/5] Add SessionStart hook to check Claude Code version Adds a version check that runs at session start and warns users if their Claude Code version is below the minimum (2.1.14). Older versions have known bugs that may cause issues with DeepWork functionality. The warning is printed to stderr with clear messaging about the issue and a suggestion to run /update to update Claude Code. --- .claude/settings.json | 11 +++ src/deepwork/hooks/check_version.sh | 121 ++++++++++++++++++++++++++++ 2 files changed, 132 insertions(+) create mode 100755 src/deepwork/hooks/check_version.sh diff --git a/.claude/settings.json b/.claude/settings.json index 6da3b2da..a0bf4664 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -98,6 +98,17 @@ ] }, "hooks": { + "SessionStart": [ + { + "matcher": "", + "hooks": [ + { + "type": "command", + "command": "src/deepwork/hooks/check_version.sh" + } + ] + } + ], "UserPromptSubmit": [ { "matcher": "", diff --git a/src/deepwork/hooks/check_version.sh b/src/deepwork/hooks/check_version.sh new file mode 100755 index 00000000..c13ac544 --- /dev/null +++ b/src/deepwork/hooks/check_version.sh @@ -0,0 +1,121 @@ +#!/bin/bash +# check_version.sh - SessionStart hook to check Claude Code version +# +# Warns users if their Claude Code version is below the minimum required +# version, as older versions may have bugs that affect DeepWork functionality. + +set -e + +# ============================================================================ +# MINIMUM VERSION CONFIGURATION +# ============================================================================ +MINIMUM_VERSION="2.1.14" + +# ============================================================================ +# VERSION CHECK LOGIC +# ============================================================================ + +# Get current Claude Code version +get_current_version() { + local version_output + version_output=$(claude --version 2>/dev/null) || return 1 + # Extract version number (e.g., "2.1.1" from "2.1.1 (Claude Code)") + echo "$version_output" | grep -oE '^[0-9]+\.[0-9]+\.[0-9]+' | head -1 +} + +# Compare two semantic versions +# Returns 0 if version1 >= version2, 1 otherwise +version_gte() { + local version1="$1" + local version2="$2" + + # Split versions into components + local v1_major v1_minor v1_patch + local v2_major v2_minor v2_patch + + IFS='.' read -r v1_major v1_minor v1_patch <<< "$version1" + IFS='.' read -r v2_major v2_minor v2_patch <<< "$version2" + + # Default to 0 if component is missing + v1_major=${v1_major:-0} + v1_minor=${v1_minor:-0} + v1_patch=${v1_patch:-0} + v2_major=${v2_major:-0} + v2_minor=${v2_minor:-0} + v2_patch=${v2_patch:-0} + + # Compare major version + if [ "$v1_major" -gt "$v2_major" ]; then + return 0 + elif [ "$v1_major" -lt "$v2_major" ]; then + return 1 + fi + + # Compare minor version + if [ "$v1_minor" -gt "$v2_minor" ]; then + return 0 + elif [ "$v1_minor" -lt "$v2_minor" ]; then + return 1 + fi + + # Compare patch version + if [ "$v1_patch" -ge "$v2_patch" ]; then + return 0 + else + return 1 + fi +} + +# Print warning message to stderr +print_version_warning() { + local current_version="$1" + + cat >&2 << EOF + +================================================================================ + *** CLAUDE CODE VERSION WARNING *** +================================================================================ + + Your Claude Code version: ${current_version} + Minimum recommended: ${MINIMUM_VERSION} + + IMPORTANT: Versions below the minimum have known bugs that may cause + issues with DeepWork functionality. You may experience unexpected + behavior, errors, or incomplete operations. + + ------------------------------------------------------------------------ + | | + | RECOMMENDED ACTION: Run /update to update Claude Code | + | | + ------------------------------------------------------------------------ + +================================================================================ + +EOF +} + +# ============================================================================ +# MAIN +# ============================================================================ + +main() { + local current_version + + # Get current version + current_version=$(get_current_version) + + if [ -z "$current_version" ]; then + # Could not determine version, skip check silently + exit 0 + fi + + # Check if current version is below minimum + if ! version_gte "$current_version" "$MINIMUM_VERSION"; then + print_version_warning "$current_version" + fi + + # Always exit successfully - this is informational only + exit 0 +} + +main "$@" From 08e25c354bd7ce654076e06155fa88f5771b1614 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 21 Jan 2026 20:50:38 +0000 Subject: [PATCH 2/5] Add structured output and tests for version check hook - Use hookSpecificOutput.additionalContext to pass version warning to Claude's context, enabling it to inform users appropriately - Keep visual stderr warning for immediate user visibility - Add comprehensive test suite (17 tests) covering version comparison, warning output, hook conformance, and edge cases - Fix graceful handling when claude command is not available --- src/deepwork/hooks/check_version.sh | 49 +++- .../shell_script_tests/test_check_version.py | 211 ++++++++++++++++++ 2 files changed, 251 insertions(+), 9 deletions(-) create mode 100644 tests/shell_script_tests/test_check_version.py diff --git a/src/deepwork/hooks/check_version.sh b/src/deepwork/hooks/check_version.sh index c13ac544..ea9d7867 100755 --- a/src/deepwork/hooks/check_version.sh +++ b/src/deepwork/hooks/check_version.sh @@ -3,8 +3,9 @@ # # Warns users if their Claude Code version is below the minimum required # version, as older versions may have bugs that affect DeepWork functionality. - -set -e +# +# Uses hookSpecificOutput.additionalContext to pass the warning to Claude's +# context so it can inform the user appropriately. # ============================================================================ # MINIMUM VERSION CONFIGURATION @@ -66,8 +67,17 @@ version_gte() { fi } -# Print warning message to stderr -print_version_warning() { +# Generate warning message +get_warning_message() { + local current_version="$1" + + cat << EOF +CLAUDE CODE VERSION WARNING: Your version (${current_version}) is below the minimum recommended (${MINIMUM_VERSION}). Older versions have known bugs that may cause issues with DeepWork. RECOMMENDED: Run /update to update Claude Code. +EOF +} + +# Print visual warning to stderr for immediate user visibility +print_stderr_warning() { local current_version="$1" cat >&2 << EOF @@ -94,27 +104,48 @@ print_version_warning() { EOF } +# Output JSON with additional context for Claude +output_json_with_context() { + local context="$1" + # Escape special characters for JSON + local escaped_context + escaped_context=$(echo "$context" | sed 's/\\/\\\\/g; s/"/\\"/g; s/\t/\\t/g' | tr '\n' ' ') + + cat << EOF +{"hookSpecificOutput":{"hookEventName":"SessionStart","additionalContext":"${escaped_context}"}} +EOF +} + # ============================================================================ # MAIN # ============================================================================ main() { local current_version + local warning_message - # Get current version - current_version=$(get_current_version) + # Get current version (don't exit on failure) + current_version=$(get_current_version) || current_version="" if [ -z "$current_version" ]; then - # Could not determine version, skip check silently + # Could not determine version, output empty JSON and exit + echo '{}' exit 0 fi # Check if current version is below minimum if ! version_gte "$current_version" "$MINIMUM_VERSION"; then - print_version_warning "$current_version" + # Print visual warning to stderr + print_stderr_warning "$current_version" + + # Output JSON with context for Claude + warning_message=$(get_warning_message "$current_version") + output_json_with_context "$warning_message" + else + # Version is OK, output empty JSON + echo '{}' fi - # Always exit successfully - this is informational only exit 0 } diff --git a/tests/shell_script_tests/test_check_version.py b/tests/shell_script_tests/test_check_version.py new file mode 100644 index 00000000..9b1fb58d --- /dev/null +++ b/tests/shell_script_tests/test_check_version.py @@ -0,0 +1,211 @@ +"""Tests for check_version.sh SessionStart hook. + +Tests version checking logic, JSON output format, and warning behavior. +""" + +import os +import subprocess +import tempfile +from pathlib import Path + +import pytest + + +@pytest.fixture +def check_version_script(hooks_dir: Path) -> Path: + """Return path to check_version.sh.""" + return hooks_dir / "check_version.sh" + + +def run_check_version_with_mock_claude( + script_path: Path, + mock_version: str | None, + cwd: Path | None = None, +) -> tuple[str, str, int]: + """ + Run check_version.sh with a mocked claude command. + + Args: + script_path: Path to check_version.sh + mock_version: Version string to return from mock claude, or None for failure + cwd: Working directory + + Returns: + Tuple of (stdout, stderr, return_code) + """ + with tempfile.TemporaryDirectory() as tmpdir: + # Create mock claude command + mock_claude = Path(tmpdir) / "claude" + if mock_version is not None: + mock_claude.write_text(f'#!/bin/bash\necho "{mock_version} (Claude Code)"\n') + else: + mock_claude.write_text("#!/bin/bash\nexit 1\n") + mock_claude.chmod(0o755) + + # Prepend mock dir to PATH + env = os.environ.copy() + env["PATH"] = f"{tmpdir}:{env.get('PATH', '')}" + + result = subprocess.run( + ["bash", str(script_path)], + capture_output=True, + text=True, + cwd=cwd or tmpdir, + env=env, + ) + + return result.stdout, result.stderr, result.returncode + + +class TestVersionComparison: + """Tests for version comparison logic.""" + + def test_equal_versions(self, check_version_script: Path) -> None: + """Test that equal versions don't trigger warning.""" + # Mock version equals minimum (2.1.14) + stdout, stderr, code = run_check_version_with_mock_claude(check_version_script, "2.1.14") + + assert code == 0 + assert "WARNING" not in stderr + + def test_greater_patch_version(self, check_version_script: Path) -> None: + """Test that greater patch version doesn't trigger warning.""" + stdout, stderr, code = run_check_version_with_mock_claude(check_version_script, "2.1.15") + + assert code == 0 + assert "WARNING" not in stderr + + def test_greater_minor_version(self, check_version_script: Path) -> None: + """Test that greater minor version doesn't trigger warning.""" + stdout, stderr, code = run_check_version_with_mock_claude(check_version_script, "2.2.0") + + assert code == 0 + assert "WARNING" not in stderr + + def test_greater_major_version(self, check_version_script: Path) -> None: + """Test that greater major version doesn't trigger warning.""" + stdout, stderr, code = run_check_version_with_mock_claude(check_version_script, "3.0.0") + + assert code == 0 + assert "WARNING" not in stderr + + def test_lesser_patch_version(self, check_version_script: Path) -> None: + """Test that lesser patch version triggers warning.""" + stdout, stderr, code = run_check_version_with_mock_claude(check_version_script, "2.1.13") + + assert code == 0 + assert "WARNING" in stderr + assert "2.1.13" in stderr # Shows current version + + def test_lesser_minor_version(self, check_version_script: Path) -> None: + """Test that lesser minor version triggers warning.""" + stdout, stderr, code = run_check_version_with_mock_claude(check_version_script, "2.0.99") + + assert code == 0 + assert "WARNING" in stderr + + def test_lesser_major_version(self, check_version_script: Path) -> None: + """Test that lesser major version triggers warning.""" + stdout, stderr, code = run_check_version_with_mock_claude(check_version_script, "1.9.99") + + assert code == 0 + assert "WARNING" in stderr + + +class TestWarningOutput: + """Tests for warning message content.""" + + def test_warning_contains_current_version(self, check_version_script: Path) -> None: + """Test that warning shows the current version.""" + stdout, stderr, code = run_check_version_with_mock_claude(check_version_script, "2.0.0") + + assert "2.0.0" in stderr + + def test_warning_contains_minimum_version(self, check_version_script: Path) -> None: + """Test that warning shows the minimum version.""" + stdout, stderr, code = run_check_version_with_mock_claude(check_version_script, "2.0.0") + + assert "2.1.14" in stderr + + def test_warning_suggests_update(self, check_version_script: Path) -> None: + """Test that warning suggests running /update.""" + stdout, stderr, code = run_check_version_with_mock_claude(check_version_script, "2.0.0") + + assert "/update" in stderr + + def test_warning_mentions_bugs(self, check_version_script: Path) -> None: + """Test that warning mentions bugs in older versions.""" + stdout, stderr, code = run_check_version_with_mock_claude(check_version_script, "2.0.0") + + assert "bugs" in stderr.lower() + + +class TestHookConformance: + """Tests for Claude Code hook format compliance.""" + + def test_always_exits_zero(self, check_version_script: Path) -> None: + """Test that script always exits 0 (informational only).""" + # Test with warning + stdout, stderr, code = run_check_version_with_mock_claude(check_version_script, "2.0.0") + assert code == 0 + + # Test without warning + stdout, stderr, code = run_check_version_with_mock_claude(check_version_script, "3.0.0") + assert code == 0 + + def test_outputs_valid_json_when_version_ok(self, check_version_script: Path) -> None: + """Test that stdout is valid JSON when version is OK.""" + import json + + stdout, stderr, code = run_check_version_with_mock_claude(check_version_script, "3.0.0") + + # Should output empty JSON object + output = json.loads(stdout.strip()) + assert output == {} + + def test_outputs_structured_json_when_version_low(self, check_version_script: Path) -> None: + """Test that stdout has hookSpecificOutput when version is low.""" + import json + + stdout, stderr, code = run_check_version_with_mock_claude(check_version_script, "2.0.0") + + output = json.loads(stdout.strip()) + assert "hookSpecificOutput" in output + assert output["hookSpecificOutput"]["hookEventName"] == "SessionStart" + assert "additionalContext" in output["hookSpecificOutput"] + assert "VERSION WARNING" in output["hookSpecificOutput"]["additionalContext"] + + def test_warning_goes_to_stderr_and_stdout(self, check_version_script: Path) -> None: + """Test that warning is on stderr (visual) and stdout (context).""" + stdout, stderr, code = run_check_version_with_mock_claude(check_version_script, "2.0.0") + + # Visual warning should be in stderr + assert "WARNING" in stderr + # JSON with context should be in stdout + assert "hookSpecificOutput" in stdout + + +class TestEdgeCases: + """Tests for edge cases and error handling.""" + + def test_claude_command_not_found(self, check_version_script: Path) -> None: + """Test graceful handling when claude command fails.""" + stdout, stderr, code = run_check_version_with_mock_claude( + check_version_script, + None, # Mock failure + ) + + # Should exit 0 and output JSON even if version check fails + assert code == 0 + assert stdout.strip() == "{}" + # No warning since we couldn't determine version + assert "WARNING" not in stderr + + def test_version_with_extra_text(self, check_version_script: Path) -> None: + """Test parsing version from output with extra text.""" + # Real output format: "2.1.1 (Claude Code)" + stdout, stderr, code = run_check_version_with_mock_claude(check_version_script, "2.1.14") + + assert code == 0 + # Version 2.1.14 equals minimum, no warning + assert "WARNING" not in stderr From 76b771520258d434d82ad1cb2d779ed662ef6175 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 21 Jan 2026 21:08:58 +0000 Subject: [PATCH 3/5] Add re-entry guard to version check hook Prevent the hook from running multiple times per session by checking an environment variable at the start. SessionStart hooks can trigger on resume, clear, etc., and we only want to show the warning once. --- src/deepwork/hooks/check_version.sh | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/deepwork/hooks/check_version.sh b/src/deepwork/hooks/check_version.sh index ea9d7867..2b1b7fc5 100755 --- a/src/deepwork/hooks/check_version.sh +++ b/src/deepwork/hooks/check_version.sh @@ -7,6 +7,21 @@ # Uses hookSpecificOutput.additionalContext to pass the warning to Claude's # context so it can inform the user appropriately. +# ============================================================================ +# RE-ENTRY GUARD +# ============================================================================ +# SessionStart hooks can be triggered multiple times in a session (on resume, +# clear, etc.). We only want to show the version warning once per session to +# avoid spamming the user. We use an environment variable to track whether +# we've already run. Note: This relies on the parent process preserving env +# vars across hook invocations within the same session. +if [ -n "$DEEPWORK_VERSION_CHECK_DONE" ]; then + # Already checked version this session, exit silently with empty JSON + echo '{}' + exit 0 +fi +export DEEPWORK_VERSION_CHECK_DONE=1 + # ============================================================================ # MINIMUM VERSION CONFIGURATION # ============================================================================ From 0336430ab2cf8b1c4aed4c8955b2462587078d93 Mon Sep 17 00:00:00 2001 From: Noah Horton Date: Wed, 21 Jan 2026 14:18:58 -0700 Subject: [PATCH 4/5] Improved messaging --- src/deepwork/hooks/check_version.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/deepwork/hooks/check_version.sh b/src/deepwork/hooks/check_version.sh index 2b1b7fc5..8ceb96e7 100755 --- a/src/deepwork/hooks/check_version.sh +++ b/src/deepwork/hooks/check_version.sh @@ -87,7 +87,7 @@ get_warning_message() { local current_version="$1" cat << EOF -CLAUDE CODE VERSION WARNING: Your version (${current_version}) is below the minimum recommended (${MINIMUM_VERSION}). Older versions have known bugs that may cause issues with DeepWork. RECOMMENDED: Run /update to update Claude Code. +CLAUDE CODE VERSION WARNING: Your version (${current_version}) is below the minimum recommended (${MINIMUM_VERSION}). Older versions have known bugs that may cause issues with DeepWork. RECOMMENDED: Update your version of Claude Code. TELL THE USER ABOUT THIS ISSUE LOUDLY!!! EOF } @@ -110,7 +110,7 @@ print_stderr_warning() { ------------------------------------------------------------------------ | | - | RECOMMENDED ACTION: Run /update to update Claude Code | + | RECOMMENDED ACTION: Update your version of Claude Code | | | ------------------------------------------------------------------------ From bb5f93be79489525171417ae94aaa58530fc42c1 Mon Sep 17 00:00:00 2001 From: Noah Horton Date: Wed, 21 Jan 2026 14:26:30 -0700 Subject: [PATCH 5/5] Fix test assertion to match actual script output The test expected `/update` in stderr but the script outputs "Update your version of Claude Code" instead. Co-Authored-By: Claude Opus 4.5 --- tests/shell_script_tests/test_check_version.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/shell_script_tests/test_check_version.py b/tests/shell_script_tests/test_check_version.py index 9b1fb58d..2fe3c44d 100644 --- a/tests/shell_script_tests/test_check_version.py +++ b/tests/shell_script_tests/test_check_version.py @@ -128,10 +128,10 @@ def test_warning_contains_minimum_version(self, check_version_script: Path) -> N assert "2.1.14" in stderr def test_warning_suggests_update(self, check_version_script: Path) -> None: - """Test that warning suggests running /update.""" + """Test that warning suggests updating Claude Code.""" stdout, stderr, code = run_check_version_with_mock_claude(check_version_script, "2.0.0") - assert "/update" in stderr + assert "Update your version of Claude Code" in stderr def test_warning_mentions_bugs(self, check_version_script: Path) -> None: """Test that warning mentions bugs in older versions."""