Skip to content

feat: stderr status line + show-cache command for post-compact visibility#31

Merged
claude[bot] merged 9 commits intomainfrom
claude/show-cache-20260226-2151
Feb 27, 2026
Merged

feat: stderr status line + show-cache command for post-compact visibility#31
claude[bot] merged 9 commits intomainfrom
claude/show-cache-20260226-2151

Conversation

@greynewell
Copy link
Contributor

@greynewell greynewell commented Feb 27, 2026

Summary

  • Terminal status line: uncompact-hook.sh now writes [uncompact] Context injected (~N tokens) to stderr during compact, visible in the terminal
  • uncompact show-cache command: new subcommand that reads the cached context bomb and emits it to stdout (one-shot, deletes cache after), replacing the bash show-hook.sh wrapper
  • UserPromptSubmit hook via uncompact install: install now also adds a UserPromptSubmit → uncompact show-cache hook to settings.json so the context bomb is replayed into AI context on the first message after compact
  • Idempotent hook merging: mergeHooks now deduplicates by command substring so re-running uncompact install is safe regardless of partial prior installs

Why UserPromptSubmit must be in settings.json

Plugin hooks.json does not fire UserPromptSubmit hooks — only SessionStart, PreCompact, and PostToolUse work from the plugin. uncompact install now writes the hook directly to settings.json.

Test plan

  • Run /compact, observe [uncompact] Context injected (~N tokens) in the terminal output
  • Send a message after compact, confirm uncompact show-cache replays the context bomb into AI context
  • Run uncompact install on a clean settings.json, verify both Stop and UserPromptSubmit hooks are written
  • Run uncompact install again on an already-installed settings.json, verify no duplicates

@claude please implement this

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added a show-cache command to replay a one-shot cached context.
    • Hook output now shows a contextual status line with approximate token counts.
  • Bug Fixes

    • Consumes/clears the temporary cache after replay to prevent duplicate/context double-injection.
  • Chores

    • Made hook installation/merge idempotent and more robust to duplicates.
    • Expanded CI workflow permissions to allow automated bot triggers.

…lity

- uncompact-hook.sh: emit [uncompact] Context injected (~N tokens) to stderr
  so the user sees a status line in the terminal during compact
- cmd/showcache.go: new `uncompact show-cache` command that reads the cached
  context bomb from the temp file and emits it to stdout (one-shot), then
  deletes the cache
- hooks/hooks.json: update UserPromptSubmit hook to use `uncompact show-cache`
  instead of the bash wrapper script
- internal/hooks/hooks.go: add UserPromptSubmit → uncompact show-cache to the
  hooks installed by `uncompact install`; refactor mergeHooks to be idempotent
  (deduplicates by command substring) and isAlreadyInstalled to check both
  Stop and UserPromptSubmit hooks
- cmd/install.go: update success message to describe both installed hooks

Co-Authored-By: Grey Newell <greyshipscode@gmail.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@coderabbitai
Copy link

coderabbitai bot commented Feb 27, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Adds a one-shot show-cache command and updates hooks and scripts: hook commands are wrapped to export PATH, installation now requires both Stop and UserPromptSubmit patterns and merges idempotently, uncompact-hook.sh writes a per-user tmp display cache with token estimates, show-cache reads/deletes that file, and CI allows claude[bot]. (50 words)

Changes

Cohort / File(s) Summary
Install messaging
cmd/install.go
Updates user-facing install output to describe both the Stop hook and the UserPromptSubmit hook (text only).
Show-cache command
cmd/showcache.go
Adds show-cache Cobra command that reads uncompact-display-<uid>.txt from tmp, deletes it, and prints contents plus approximate token count.
Hook management
internal/hooks/hooks.go
Wraps hook commands with bash -c that exports PATH, adds commandExistsInHooks helper, tightens isAlreadyInstalled to require both Stop and UserPromptSubmit patterns, and makes mergeHooks idempotent by skipping existing commands and checking multiple equivalent command variants.
Hook scripts
scripts/uncompact-hook.sh, scripts/show-hook.sh
Switches from emitting raw OUTPUT to writing/printing a DISPLAY_CACHE file and diagnostic line with approximate token counts; uncompact-hook.sh writes the cache, show-hook.sh prints cache + token estimate.
CI workflow
.github/workflows/claude.yml
Adds allowed_bots: "claude[bot]" to permit the claude[bot] user to trigger the workflow.

Sequence Diagram

sequenceDiagram
    participant User
    participant StopHook as Stop Hook\n(`uncompact-hook.sh`)
    participant Cache as Temp File\n(`/tmp/uncompact-display-<uid>.txt`)
    participant UserPrompt as UserPromptSubmit Hook
    participant ShowCmd as show-cache Command\n(`cmd/showcache`)
    participant Claude

    User->>StopHook: finishes session -> Stop hook runs
    StopHook->>Cache: write transcript + "[uncompact] Context restored (~N tokens)"
    User->>UserPrompt: sends next message -> UserPromptSubmit triggers
    UserPrompt->>ShowCmd: invokes `show-cache`
    ShowCmd->>Cache: read file, delete file
    ShowCmd->>Claude: output replayed to AI (prints context + token estimate)
    Claude->>User: responds with restored context applied
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

A tiny file, a whispered trace,
hooks that tuck the context safe,
PATH set right, tokens tallied neat,
replayed on demand—quiet and sweet. 🎈

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 57.14% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and specifically summarizes the main changes: adding a stderr status line and a show-cache command for post-compact visibility.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch claude/show-cache-20260226-2151

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (2)
cmd/showcache.go (2)

30-31: Consider logging read errors for debugging.

Right now, any read error (permission denied, I/O error, etc.) silently exits. That's fine for production, but it could make debugging tricky if something goes wrong.

Maybe stash the error somewhere (a debug log, env-gated stderr) so you can troubleshoot later? Not blocking, just a thought for future-you.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cmd/showcache.go` around lines 30 - 31, The read-error branch in
cmd/showcache.go currently swallows errors (if err != nil { return nil });
change it to record the error for debugging before returning so issues like
permission or I/O failures are visible. Update the same function to log the
error (e.g., using the existing logger or log.Printf) and gate noisy output
behind a debug/verbose flag or an env var (e.g., DEBUG) so production behavior
stays unchanged; keep the return value the same after logging. Reference the err
variable and the read branch in cmd/showcache.go when making the change.

34-38: Minor: delete happens before print - tiny edge case.

If fmt.Print somehow fails (rare but possible - broken pipe, etc.), the cache is already gone. The current order is:

  1. Delete file
  2. Print content

If you flipped it:

  1. Print content
  2. Delete file

...a failed print would leave the cache intact for retry. Probably doesn't matter in practice since fmt.Print to stdout rarely fails, but figured I'd mention it!

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cmd/showcache.go` around lines 34 - 38, The deletion happens before printing
(os.Remove(cachePath) then fmt.Print), which can leave cache gone if printing
fails; change the order in the cmd/showcache.go flow to print the cached data
first (use fmt.Print and check its returned error), and only remove the
cachePath afterward (call os.Remove(cachePath) after a successful print, and
optionally handle/log Remove errors); update the code around the print/remove
sequence accordingly (refer to the os.Remove(cachePath) call and the
fmt.Print(string(data)) call).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@cmd/showcache.go`:
- Around line 23-24: showCacheHandler constructs cachePath using os.Getuid(),
which returns -1 on Windows and leads to mismatched filenames; update
showCacheHandler to detect when os.Getuid() == -1 and fall back to a
Windows-safe identifier (e.g., the current username from
os/user.Current().Username or a fixed literal like "windows") before calling
fmt.Sprintf and filepath.Join so the generated filename matches what hooks
expect cross-platform; reference symbols: showCacheHandler, cachePath,
os.Getuid, fmt.Sprintf, filepath.Join, and consider using os/user.Current() for
the fallback.

---

Nitpick comments:
In `@cmd/showcache.go`:
- Around line 30-31: The read-error branch in cmd/showcache.go currently
swallows errors (if err != nil { return nil }); change it to record the error
for debugging before returning so issues like permission or I/O failures are
visible. Update the same function to log the error (e.g., using the existing
logger or log.Printf) and gate noisy output behind a debug/verbose flag or an
env var (e.g., DEBUG) so production behavior stays unchanged; keep the return
value the same after logging. Reference the err variable and the read branch in
cmd/showcache.go when making the change.
- Around line 34-38: The deletion happens before printing (os.Remove(cachePath)
then fmt.Print), which can leave cache gone if printing fails; change the order
in the cmd/showcache.go flow to print the cached data first (use fmt.Print and
check its returned error), and only remove the cachePath afterward (call
os.Remove(cachePath) after a successful print, and optionally handle/log Remove
errors); update the code around the print/remove sequence accordingly (refer to
the os.Remove(cachePath) call and the fmt.Print(string(data)) call).

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f07da89 and d10b23e.

📒 Files selected for processing (5)
  • cmd/install.go
  • cmd/showcache.go
  • hooks/hooks.json
  • internal/hooks/hooks.go
  • scripts/uncompact-hook.sh

greynewell and others added 2 commits February 26, 2026 22:08
…us on replay

Hooks run with a restricted environment where ~/go/bin is not in PATH,
causing bare 'uncompact show-cache' to fail with "command not found".

- hooks.json: UserPromptSubmit → show-hook.sh (no binary PATH dependency)
- hooks.go: wrap install-time hook commands in bash with explicit PATH
- show-hook.sh + showcache.go: emit '[uncompact] Context replayed' to stderr
  so the user sees confirmation when context is replayed on UserPromptSubmit

Co-Authored-By: Grey Newell <greyshipscode@gmail.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
claude-auto-assign.yml posts @claude comments as claude[bot], which then
triggers claude.yml. The action rejected these with "non-human actor" error
because allowed_bots was not configured.

Co-Authored-By: Grey Newell <greyshipscode@gmail.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@internal/hooks/hooks.go`:
- Around line 201-211: Dedup logic misses legacy/variant forms (e.g., "uncompact
run" vs "bash -c '...; uncompact run'") so Stop hooks can be appended twice;
update the check used when iterating hooks in the loop (the call to
commandExistsInHooks(result[event], cmd.Command)) to normalize/compare commands
by their canonical operation (e.g., strip wrapping shells and trailing prefixes,
unquote, or compare the final command tokens) or extend commandExistsInHooks to
detect legacy variants (check for contains/endsWith of the core command like
"uncompact run" inside the candidate command); keep using the same symbols
hook.Hooks, cmd.Command and result[event] but ensure the comparison finds
semantic matches so duplicates are not appended.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d10b23e and fff8e23.

📒 Files selected for processing (4)
  • .github/workflows/claude.yml
  • cmd/showcache.go
  • internal/hooks/hooks.go
  • scripts/show-hook.sh
🚧 Files skipped from review as they are similar to previous changes (1)
  • cmd/showcache.go

- uncompact-hook.sh: emit context + status via printf to stdout;
  remove cache write (SessionStart:compact handles injection directly);
  rm -f display cache to prevent UserPromptSubmit double-injection
- show-hook.sh: move status from stderr to stdout, use printf
- showcache.go: consolidate output into single printf to stdout

Hook stderr is silently discarded by Claude Code — only stdout is
injected into the AI's context. Status line now appears in context
on SessionStart:compact without the user needing to send a message.

Co-Authored-By: Grey Newell <greyshipscode@gmail.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
scripts/show-hook.sh (1)

15-17: ⚠️ Potential issue | 🟡 Minor

Comment claims atomicity, but cat + rm isn't atomic.

Hey! Quick heads up: the comment says "Read and remove atomically" but these are actually two separate operations. If two instances of this script ran at the exact same moment (super rare, but possible), they could both cat the file before either does the rm.

Think of it like this: imagine two people both reaching for the last cookie on a plate. One grabs it, but before they can pull the plate away, the other person also grabs... air, because the cookie's gone. Here, both could grab the cookie.

In practice this is unlikely since UserPromptSubmit shouldn't fire concurrently, and the worst case is just seeing the context twice—not a big deal. But the comment is a bit misleading.

If you want true atomicity, you could mv the file first:

TEMP_READ="$(mktemp)"
mv "$DISPLAY_CACHE" "$TEMP_READ" 2>/dev/null || exit 0
OUTPUT="$(cat "$TEMP_READ")"
rm -f "$TEMP_READ"

Or just update the comment to be more accurate. Up to you!

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/show-hook.sh` around lines 15 - 17, The comment claims atomic
read+remove but the current sequence (reading DISPLAY_CACHE into OUTPUT then rm)
is not atomic; fix by moving DISPLAY_CACHE to a temporary file before reading to
ensure only one process can take the file: create a temp with mktemp, mv
DISPLAY_CACHE to that temp (if mv fails, exit 0), then read the temp into OUTPUT
and remove the temp; reference DISPLAY_CACHE and OUTPUT in the update and
replace the existing cat + rm sequence with this move-then-read pattern (or
alternatively update the comment to remove the “atomically” claim if you prefer
not to change behavior).
🧹 Nitpick comments (1)
scripts/uncompact-hook.sh (1)

48-49: Smart defensive cleanup—prevents double-injection.

This is a nice touch: even if there's a stale cache file from a previous run that didn't clean up properly, this rm -f ensures UserPromptSubmit won't accidentally replay old context.

One tiny nitpick: the file doesn't end with a newline after line 49. POSIX technically says text files should end with a newline, and some tools can be grumpy about it. Not a big deal, but easy to fix if you want to be squeaky clean.

,

🧹 Optional: add trailing newline
 # Clean up any stale display cache so UserPromptSubmit doesn't double-inject.
 rm -f "$DISPLAY_CACHE"
+
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/uncompact-hook.sh` around lines 48 - 49, The file ends without a
trailing newline after the final rm -f "$DISPLAY_CACHE" line; open
scripts/uncompact-hook.sh and add a single newline at EOF so the file terminates
with a newline character (preserving the existing rm -f "$DISPLAY_CACHE" line
and any surrounding comments) to satisfy POSIX text-file conventions and avoid
tool warnings related to DISPLAY_CACHE/UserPromptSubmit handling.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@scripts/show-hook.sh`:
- Around line 15-17: The comment claims atomic read+remove but the current
sequence (reading DISPLAY_CACHE into OUTPUT then rm) is not atomic; fix by
moving DISPLAY_CACHE to a temporary file before reading to ensure only one
process can take the file: create a temp with mktemp, mv DISPLAY_CACHE to that
temp (if mv fails, exit 0), then read the temp into OUTPUT and remove the temp;
reference DISPLAY_CACHE and OUTPUT in the update and replace the existing cat +
rm sequence with this move-then-read pattern (or alternatively update the
comment to remove the “atomically” claim if you prefer not to change behavior).

---

Nitpick comments:
In `@scripts/uncompact-hook.sh`:
- Around line 48-49: The file ends without a trailing newline after the final rm
-f "$DISPLAY_CACHE" line; open scripts/uncompact-hook.sh and add a single
newline at EOF so the file terminates with a newline character (preserving the
existing rm -f "$DISPLAY_CACHE" line and any surrounding comments) to satisfy
POSIX text-file conventions and avoid tool warnings related to
DISPLAY_CACHE/UserPromptSubmit handling.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between fff8e23 and 40c7473.

📒 Files selected for processing (3)
  • cmd/showcache.go
  • scripts/show-hook.sh
  • scripts/uncompact-hook.sh
🚧 Files skipped from review as they are similar to previous changes (1)
  • cmd/showcache.go

greynewell and others added 3 commits February 26, 2026 22:32
…ranscript

SessionStart:compact stdout only reaches Claude as a system-reminder (not
visible to the user in the chat transcript). Writing to the display cache
instead means the UserPromptSubmit hook will show the context bomb as a
visible hook output block in the transcript on the user's next message.

Co-Authored-By: Grey Newell <greyshipscode@gmail.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Fall back to username when os.Getuid() returns -1 (Windows) so the
cache filename matches what the hook script expects cross-platform.

Co-Authored-By: Grey Newell <greyshipscode@gmail.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Extend mergeHooks dedup to also match legacy/variant command forms
(e.g. bare "uncompact run") so upgrading from an old install does not
append a duplicate Stop or UserPromptSubmit hook.

Co-Authored-By: Grey Newell <greyshipscode@gmail.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@internal/hooks/hooks.go`:
- Around line 200-210: The loop that builds dedupe matches inside the toAdd ->
for event, hooks -> for _, hook -> for _, cmd range is treating legacy commands
(e.g. "show-hook.sh"/"uncompact-hook.sh") as equivalents which causes the new
canonical commands not to be written; fix by normalizing/migrating legacy values
before creating the matches list: inside the loop over hook.Hooks (the
cmd.Command handling), map legacy names ("show-hook.sh" -> "uncompact
show-cache", "uncompact-hook.sh" -> "uncompact run") to their canonical commands
(or assign to a normalized variable) and then build matches/dedupe only around
the canonical form (adding any legacy names only for migration logging if
desired) so the new "UserPromptSubmit" command is added and legacy entries are
replaced rather than causing skip=true.
- Around line 36-43: The hardcoded bash wrapper for Command entries (e.g., the
auto-install hooks under "UserPromptSubmit" that create Command{Type: "command",
Command: `bash -c '...; uncompact ...'`}) will fail on Windows; update the code
that builds these Command.Command strings to branch on runtime.GOOS: for
"windows" emit a PowerShell-style invocation (e.g., use powershell.exe -Command
"<set $env:PATH or just call uncompact ...>") or, simpler, emit plain "uncompact
run" and "uncompact show-cache" for all platforms to avoid shell-specific
wrappers. Modify the hook construction logic that generates those Command
entries (look for where the Command structs are created for "UserPromptSubmit" /
auto-install hooks) to use runtime.GOOS to choose the appropriate command string
instead of always using bash -c.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 21959f7 and 0fb3da2.

📒 Files selected for processing (2)
  • cmd/showcache.go
  • internal/hooks/hooks.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • cmd/showcache.go

Comment on lines +36 to +43
{Type: "command", Command: `bash -c 'export PATH="$HOME/go/bin:$HOME/.local/bin:/usr/local/bin:/opt/homebrew/bin:$PATH"; uncompact run'`},
},
},
},
"UserPromptSubmit": {
{
Hooks: []Command{
{Type: "command", Command: `bash -c 'export PATH="$HOME/go/bin:$HOME/.local/bin:/usr/local/bin:/opt/homebrew/bin:$PATH"; uncompact show-cache'`},
Copy link

@coderabbitai coderabbitai bot Feb 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, let's look at the file structure and understand the project
cd internal/hooks && pwd && ls -la

Repository: supermodeltools/Uncompact

Length of output: 271


🏁 Script executed:

# Read the hooks.go file to understand the hook execution mechanism
cat -n internal/hooks/hooks.go | head -100

Repository: supermodeltools/Uncompact

Length of output: 3454


🏁 Script executed:

# Check if there's any information about how hooks are executed (search for Command type usage)
rg -A 5 -B 5 "Type.*command" internal/hooks/hooks.go

Repository: supermodeltools/Uncompact

Length of output: 605


🏁 Script executed:

# Look for Windows-specific handling in the codebase
rg -i "windows|powershell|cmd\\.exe|shell|bash" --type-list | head -5
rg -i "windows|powershell|cmd" . --type go

Repository: supermodeltools/Uncompact

Length of output: 4942


🏁 Script executed:

# Search for how the Command type is defined and used
rg "type Command struct" -A 10 .

Repository: supermodeltools/Uncompact

Length of output: 723


🏁 Script executed:

# Check if there's documentation about this tool
fd -e md | xargs grep -l "hook" | head -5

Repository: supermodeltools/Uncompact

Length of output: 78


🏁 Script executed:

# Check the README for information about hooks and Windows support
head -200 README.md

Repository: supermodeltools/Uncompact

Length of output: 6000


🏁 Script executed:

# Search for any additional context about hook execution or Windows compatibility
rg -i "hook|windows|bash|shell|execute" internal/hooks/hooks.go -B 2 -A 2

Repository: supermodeltools/Uncompact

Length of output: 4417


🏁 Script executed:

# Look at the full hooks.go file to understand if there's any Windows-specific logic for hooks
tail -n +100 internal/hooks/hooks.go

Repository: supermodeltools/Uncompact

Length of output: 5070


🌐 Web query:

Claude Code hooks guide how command type hooks executed Windows

💡 Result:

How type: "command" hooks are executed (incl. Windows)

  • They run as OS shell commands: a command hook is defined with {"type":"command","command":"..."} and Claude Code executes that shell command when the hook event fires. (docs.anthropic.com)
  • Working dir + env: handlers run in the current directory with Claude Code’s environment (and you can rely on env vars like $CLAUDE_PROJECT_DIR when Claude Code spawns the hook). (docs.anthropic.com)
  • Input/Output contract:
    • JSON is passed on stdin to your command hook. (docs.anthropic.com)
    • Your hook can signal outcomes via exit codes and/or return structured JSON on stdout (for events that support decisions). (docs.anthropic.com)

Windows: what to put in command

Claude Code doesn’t require “PowerShell hooks” specifically—you just run PowerShell by calling it explicitly in the command string, e.g.:

{
  "type": "command",
  "command": "powershell.exe -Command \"[System.Reflection.Assembly]::LoadWithPartialName('System.Windows.Forms'); [System.Windows.Forms.MessageBox]::Show('Claude Code needs your attention', 'Claude Code')\""
}

This example is shown in the official hooks guide under Windows (PowerShell). (docs.anthropic.com)

(Similarly, on Windows you can use cmd.exe /c ... or bash -c ... if those are available on your PATH—the key is: whatever you write in command is what gets executed as the shell command.) (docs.anthropic.com)

Citations:


🌐 Web query:

Claude Code documentation hook command execution shell Windows

💡 Result:

Claude Code hooks are configured in ~/.claude/settings.json or .claude/settings.json and each hook handler can run a "type": "command" with a "command" string (the hook receives JSON on stdin). [1]

On Windows, the docs show you should invoke the shell you want explicitly—e.g. run PowerShell by setting command to powershell.exe -Command "..." (example shown for a Notification hook). [2]

Example (Windows / PowerShell):

{
  "hooks": {
    "Notification": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "powershell.exe -Command \"[System.Reflection.Assembly]::LoadWithPartialName('System.Windows.Forms'); [System.Windows.Forms.MessageBox]::Show('Claude Code needs your attention', 'Claude Code')\""
          }
        ]
      }
    ]
  }
}

You can also reference project-local scripts using CLAUDE_PROJECT_DIR in your command. [1]

Sources: Hooks reference [1], Hooks guide (Windows/PowerShell example) [2].


Generate platform-aware hook commands for Windows compatibility.

Lines 36 and 43 hardcode bash -c '...' in the auto-install code, but bash may not be available on Windows systems by default (unless Git Bash, WSL, or similar is installed). When users run uncompact install on Windows, these hooks will fail unless bash exists on their PATH.

Since Claude Code's official documentation shows users must invoke the shell explicitly (e.g., powershell.exe -Command "..." on Windows), use runtime.GOOS (already imported) to generate the appropriate shell command for each OS:

  • Linux/macOS: bash -c 'export PATH=...; uncompact ...'
  • Windows: powershell.exe -Command "$env:PATH = ...; uncompact ..." (or just uncompact run if PATH isn't needed on Windows)

Alternatively, simplify both platforms to just uncompact run and uncompact show-cache (as shown in your README manual installation), since the PATH override may not be necessary when users already have these binaries in their PATH or use the plugin system.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/hooks/hooks.go` around lines 36 - 43, The hardcoded bash wrapper for
Command entries (e.g., the auto-install hooks under "UserPromptSubmit" that
create Command{Type: "command", Command: `bash -c '...; uncompact ...'`}) will
fail on Windows; update the code that builds these Command.Command strings to
branch on runtime.GOOS: for "windows" emit a PowerShell-style invocation (e.g.,
use powershell.exe -Command "<set $env:PATH or just call uncompact ...>") or,
simpler, emit plain "uncompact run" and "uncompact show-cache" for all platforms
to avoid shell-specific wrappers. Modify the hook construction logic that
generates those Command entries (look for where the Command structs are created
for "UserPromptSubmit" / auto-install hooks) to use runtime.GOOS to choose the
appropriate command string instead of always using bash -c.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Acknowledged. The bash -c wrapper is intentional for now — the README documents Linux/macOS as the primary platform and bash availability is assumed. Generating platform-specific PowerShell commands is a larger design change. Leaving for a follow-up issue if Windows support becomes a priority.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Skipped: comment is from another GitHub bot.

Comment on lines 200 to +210
for event, hooks := range toAdd {
result[event] = append(result[event], hooks...)
for _, hook := range hooks {
skip := false
for _, cmd := range hook.Hooks {
matches := []string{cmd.Command}
switch event {
case "Stop":
matches = append(matches, "uncompact run", "uncompact-hook.sh")
case "UserPromptSubmit":
matches = append(matches, "uncompact show-cache", "show-hook.sh")
}
Copy link

@coderabbitai coderabbitai bot Feb 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Legacy show-hook.sh dedupe blocks migration to uncompact show-cache.

At Line 209, treating "show-hook.sh" as an installed equivalent makes skip=true for legacy configs, so the new UserPromptSubmit command is never written. This leaves stale hook commands after upgrade.

💡 Suggested fix (migrate legacy command, then dedupe on current form)
+func replaceLegacyShowHook(hooks []Hook, replacement string) ([]Hook, bool) {
+	replaced := false
+	for i := range hooks {
+		for j := range hooks[i].Hooks {
+			cmd := hooks[i].Hooks[j].Command
+			if strings.Contains(cmd, "show-hook.sh") && !strings.Contains(cmd, "uncompact show-cache") {
+				hooks[i].Hooks[j].Command = replacement
+				replaced = true
+			}
+		}
+	}
+	return hooks, replaced
+}
+
 func mergeHooks(existing, toAdd map[string][]Hook) map[string][]Hook {
 	result := make(map[string][]Hook)
 	for k, v := range existing {
 		result[k] = v
 	}
 	for event, hooks := range toAdd {
 		for _, hook := range hooks {
 			skip := false
 			for _, cmd := range hook.Hooks {
+				if event == "UserPromptSubmit" {
+					if updated, replaced := replaceLegacyShowHook(result[event], cmd.Command); replaced {
+						result[event] = updated
+						skip = true
+						break
+					}
+				}
 				matches := []string{cmd.Command}
 				switch event {
 				case "Stop":
 					matches = append(matches, "uncompact run", "uncompact-hook.sh")
 				case "UserPromptSubmit":
-					matches = append(matches, "uncompact show-cache", "show-hook.sh")
+					matches = append(matches, "uncompact show-cache")
 				}
 				if commandExistsInHooks(result[event], matches...) {
 					skip = true
 					break
 				}
 			}
 			if !skip {
 				result[event] = append(result[event], hook)
 			}
 		}
 	}
 	return result
 }

Also applies to: 211-218

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/hooks/hooks.go` around lines 200 - 210, The loop that builds dedupe
matches inside the toAdd -> for event, hooks -> for _, hook -> for _, cmd range
is treating legacy commands (e.g. "show-hook.sh"/"uncompact-hook.sh") as
equivalents which causes the new canonical commands not to be written; fix by
normalizing/migrating legacy values before creating the matches list: inside the
loop over hook.Hooks (the cmd.Command handling), map legacy names
("show-hook.sh" -> "uncompact show-cache", "uncompact-hook.sh" -> "uncompact
run") to their canonical commands (or assign to a normalized variable) and then
build matches/dedupe only around the canonical form (adding any legacy names
only for migration logging if desired) so the new "UserPromptSubmit" command is
added and legacy entries are replaced rather than causing skip=true.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in commit cef8835 — removed "show-hook.sh" from the UserPromptSubmit dedup matches. There is no legacy show-hook.sh script, so it should not block the new uncompact show-cache hook from being installed.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Skipped: comment is from another GitHub bot.

greynewell and others added 2 commits February 26, 2026 22:50
UserPromptSubmit hook stdout is never visible in the Claude Code chat UI —
it only injects context for Claude as a system-reminder. To give users
visible confirmation after compact, add --post-compact to uncompact run.

When --post-compact is set, the context bomb includes a brief instruction
asking Claude to open its first post-compact response with:
  "✓ Uncompact: context restored (~N tokens)"

This is the only reliable user-visible signal available: Claude's response
is shown in the transcript; hook stdout is not.

Co-Authored-By: Grey Newell <greyshipscode@gmail.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Remove "show-hook.sh" from the UserPromptSubmit dedup matches: there is
no legacy show-hook.sh script, so treating it as equivalent to
"uncompact show-cache" would incorrectly block the new hook from being
installed on clean systems.

Co-Authored-By: Grey Newell <greyshipscode@gmail.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This was referenced Feb 27, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant