Skip to content

team-init required mode: generated hook emits flat permissionDecision, so the deny never fires #2296

Closed

Description

@SW3T0

I was setting up team mode for a repo and wanted to check that required actually blocks before rolling it out, so I had Claude Code test the generated hook. Turns out the deny doesn't work.

bin/gstack-team-init required generates .claude/hooks/check-gstack.sh. When gstack is missing it prints:

{"permissionDecision":"deny","message":"gstack is required but not installed. See stderr for install instructions."}

For PreToolUse hooks, Claude Code only reads permissionDecision from inside a hookSpecificOutput object (see the hooks reference). A flat permissionDecision isn't a documented field at any event, so this JSON gets ignored. The script also exits 0, which means the exit-code-2 blocking path doesn't apply either. Net result: the Skill call goes through and required mode doesn't enforce anything.

The odd part is that gstack already does this correctly elsewhere. hosts/claude/hooks/question-preference-hook.ts:105-115:

function deny(reason: string): void {
  process.stdout.write(
    JSON.stringify({
      hookSpecificOutput: {
        hookEventName: 'PreToolUse',
        permissionDecision: 'deny',
        permissionDecisionReason: reason,
      },
    }),
  );
  process.exit(0);
}

So it looks like the generator in bin/gstack-team-init:130 just drifted from the schema the other hooks use.

Repro

cd $(mktemp -d) && git init -q
~/.claude/skills/gstack/bin/gstack-team-init required
# simulate a machine without gstack
FAKE=$(mktemp -d); HOME="$FAKE" bash .claude/hooks/check-gstack.sh

Prints the flat JSON above.

Impact

The CLAUDE.md that team-init writes still works, since the agent follows those instructions as text. But the hook is supposed to be the hard gate and it isn't. A team that turns on required will assume enforcement they don't have.

Fix

One line in bin/gstack-team-init:130:

echo '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":"gstack is required but not installed. See stderr for install instructions."}}'

I patched my generated hook locally and verified both paths: denies when gstack is absent, still prints {} when present. test/team-mode.test.ts only asserts the hook exists and is registered, so nothing there breaks. Can open a PR if you want.

Environment

  • gstack 1.60.1.0 (a325940)
  • Windows 11, Git Bash (MSYS2)
  • Claude Code

Found on Windows but the generated line is the same on every platform.

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

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions