Skip to content

SessionEnd viz hook fails on every exit on Windows (registered unconditionally, and bare bash resolves to WSL) #27

Open

Description

@sassy-lily

Describe the bug

On Windows, the viz SessionEnd hook fails on every single exit of Claude Code, printing:

SessionEnd hook [bash ${CLAUDE_PLUGIN_ROOT}/viz/hooks/session_end.sh] failed: Hook cancelled

The hook has no work to do on Windows by design, but it is registered and spawned anyway. There are two independent causes.

To reproduce

Steps to reproduce the behavior:

Simply exit Claude Code, the error is shown every time.

Expected behavior

No error is shown.

Environment

  • Plugin: datadog 0.7.17 (installed from the anthropics/claude-plugins-official marketplace)
  • Claude Code: 2.1.250, native install (claude.exe), not the npm package
  • OS: Windows 11 Pro 10.0.26200
  • Git Bash present, WSL also present

Cause 1 — the hook is registered on every platform, but only implemented for macOS

hooks/hooks.json:2 scopes the feature to macOS:

"Datadog visualizations: … macOS only. Falls back to the sandbox URL when unavailable."

and viz/hooks/session_end.sh:13-16 is a no-op off Darwin:

case "$(uname)" in
  Darwin) forward_macos ;;
  *) ;;
esac

But hooks/hooks.json:10-14 registers the hook unconditionally:

"SessionEnd": [
  {
    "hooks": [{ "type": "command", "command": "bash", "args": ["${CLAUDE_PLUGIN_ROOT}/viz/hooks/session_end.sh"] }]
  }
]

So on Windows and Linux, Claude Code spawns a shell at every session end purely to reach a *) branch that does nothing.

Cause 2 — bare bash resolves to the WSL launcher, which cannot read the Windows plugin path

"command": "bash" is resolved against PATH. On a default Windows install that is C:\Windows\System32\bash.exe — the WSL launcher, not Git Bash:

> (Get-Command bash).Source
C:\Windows\System32\bash.exe
> bash -c uname
Linux

${CLAUDE_PLUGIN_ROOT} expands to a Windows path, which the WSL shell consumes as escape sequences. Running the hook's exact command by hand:

> bash "C:\Users\<me>\.claude\plugins\cache\claude-plugins-official\datadog\0.7.17/viz/hooks/session_end.sh"
/bin/bash: C:Users<me>.claudepluginscacheclaude-plugins-officialdatadog0.7.17/viz/hooks/session_end.sh: No such file or directory
exit=127

The drive letter and every backslash are gone. The script is never found, so even the intended no-op cannot run.

Activity

  1. ZayanKhan-12 commented on Sep 14, 2026

    @ZayanKhan-12

    Opened #29 for this: #29

    Your diagnosis was right on both counts. The piece I'd add is why bash gets resolved against PATH in the first place: it's the args key. Supplying args is what puts a hook in exec form, where Claude Code resolves command as an executable and spawns it directly with no shell — which is how you end up at C:\Windows\System32\bash.exe. Dropping args and naming the script in the command string instead switches to shell form, which routes to sh -c on macOS/Linux and to Git Bash on Windows:

    -"command": "bash", "args": ["${CLAUDE_PLUGIN_ROOT}/viz/hooks/session_end.sh"]
    +"command": "\"${CLAUDE_PLUGIN_ROOT}\"/viz/hooks/session_end.sh"

    Shell form executes the file itself rather than handing it to an interpreter, so both scripts also need mode 755 — at 644 they exit 126.

    Two notes beyond what you reported:

    forward.sh has the identical bug. Same exec-form invocation, equally broken on Windows — it just fires on visualization tool calls rather than on every exit, so it wasn't as visible. It needs the opposite treatment from session_end.sh though: off macOS it isn't a no-op, it emits the "chart is at the sandbox URL" fallback to the model, so it has to keep working rather than stop being registered.

    Cause 1 can't be fixed the way it's framed, and I think the premise doesn't hold. There's no OS conditional for hook registration. The if field takes permission-rule syntax and is only evaluated on tool events — "On other events, a hook with if set never runs." Putting if on SessionEnd would disable the hook on macOS too, the one platform it exists for. So a macOS-only hook can only be made cheap and silent, not unregistered. Once cause 2 is fixed it is exactly that: the shell starts, uname misses Darwin, the script exits 0 with no output. Cause 1 stops being a bug once cause 2 is.

    Verified on a real Windows runner rather than reasoned about — the job hands the hook a native D:\a\... plugin root through C:\Program Files\Git\bin\bash.EXE, since Git Bash reports $PWD in Unix form and an ordinary Windows job would never see the path shape that caused this:

    ok   SessionEnd exits 0 on MINGW64_NT-10.0-26100
    ok   SessionEnd prints nothing
    14 passed, 0 failed
    

    I'm an outside contributor, so it's entirely up to the maintainers whether they take that shape — and per CONTRIBUTING this repo is regenerated from an internal source, so the change would need porting there regardless. Flagging it here in case it's useful to anyone else hitting the same error in the meantime.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions