[FEATURE] Documented interactive-vs-headless signal in the SessionStart hook input (so hooks can self-gate for claude -p / nested sessions)

Status Open
Maintainer reply None cached
Activity 0 comments · opened Jul 16, 2026

Preflight Checklist

  • [x] I have searched existing requests and this feature hasn't been requested yet
  • [x] This is a single feature request (not multiple features)

Problem Statement

SessionStart hooks fire in every session — including headless claude -p and nested invocations, e.g. an app or agent that shells out to claude -p via the SDK. A SessionStart hook that is only meaningful interactively (anything that writes to stdout — which enters the model's context — or performs an interactive-only side effect) has no documented way to detect that it's in a headless/nested context and should no-op.

The documented SessionStart input fields don't capture this: source distinguishes startup/resume/clear/compact (how a session began), not interactive vs print-mode. The only signal that currently carries it is the undocumented CLAUDE_CODE_ENTRYPOINT env var (observed: cli = interactive terminal, claude-desktop = desktop app, sdk-cli = headless claude -p). Binding hook logic to an undocumented, unguaranteed variable is fragile across releases.

Two concrete harms we hit:

  1. Context injection across a layering boundary. When a product drives claude -p, an operator's user-level SessionStart hook fires inside that nested session and its stdout enters the nested model's context — a determinism/reproducibility problem for the embedding app, which can't control operator-level hooks.
  2. Side effects in headless runs. A hook that dispatches an interpreter fired inside an automated claude -p test on Windows and popped a "Select an app to open python3" OS picker — mid-test, with no interactive user present.

Proposed Solution

Add a documented field to the SessionStart hook input JSON that distinguishes interactive from non-interactive invocations. Any one of:

  • interactive: boolean — true for terminal/desktop sessions, false for claude -p/print-mode/SDK-driven; or
  • an invocation/mode enum, e.g. "interactive" | "print" (richer is fine: "cli" | "print" | "sdk" | "desktop"); or
  • promote CLAUDE_CODE_ENTRYPOINT to a documented, stable signal (its value set + stability guarantee), since it already encodes this.

Ideal UX — a hook self-gates from stdin, no env-var guessing:

import json, sys
data = json.load(sys.stdin)
if not data.get("interactive", True):   # documented field
    sys.exit(0)                          # don't run in headless/nested sessions

Alternative Solutions

  • CLAUDE_CODE_ENTRYPOINT — works today, but undocumented/unsupported; we use it only as an interim binding.
  • TTY detection (isatty) — unreliable; hooks may not inherit a TTY and print-mode vs interactive don't cleanly differ across environments.
  • --settings '{"disableAllHooks": true}' on the caller's claude -p — a good caller-side fix (the embedding app opts out), but it doesn't let a hook self-gate for arbitrary/unknown callers, and requires every embedder to know to do it.
  • source field — orthogonal (start reason, not interactivity).

Priority

High - Significant impact on productivity

Feature Category

Developer tools/SDK

Use Case Example

  1. A plugin ships a SessionStart hook that prints a status/survey block for interactive coordinator sessions.
  2. A separate product uses claude -p (SDK) to run its own read-only model over some input.
  3. The operator's plugin hook fires inside that nested claude -p, injecting the survey text into the product model's context — and on Windows pops an OS app-picker during an automated test.
  4. With a documented interactive flag, the hook reads it from stdin and early-exits in the nested case — no injection, no side effect — without depending on an undocumented env var.

Additional Context

  • Documented SessionStart input today (per code.claude.com/docs/en/hooks.md): session_id, transcript_path, cwd, source ∈ {startup,resume,clear,compact}, model, session_title, permission_mode, effort, agent_id/agent_type — none expresses interactivity.
  • Observed CLAUDE_CODE_ENTRYPOINT values (macOS + Windows probing): cli, claude-desktop, sdk-cli; CLAUDE_CODE_REMOTE unset throughout.
  • Environment: claude --version = (fill in); seen on macOS and Windows.

View original on GitHub ↗