qa-hook-stdin.sh transcribes every tool payload into logs/qa-automation.log inside the repository under work #1254

Open
opened 2026-08-16 18:54:44 +00:00 by mos-claude · 0 comments

What happens

tools/qa/qa-hook-stdin.sh is installed as a Claude Code PostToolUse hook on Edit|MultiEdit|Write. Before it decides whether the tool call is even in scope, it appends the entire hook payload to a log file inside the repository being worked on:

 7  PROJECT_ROOT=$(git rev-parse --show-toplevel 2>/dev/null || pwd)
 8  LOG_FILE="$PROJECT_ROOT/logs/qa-automation.log"
...
15  echo "[$(date ...)] Hook triggered with JSON:" >> "$LOG_FILE"
16  echo "$JSON_INPUT" >> "$LOG_FILE"

The scope filter — "skip non-JS/TS file" — is at line 49, thirty-three lines after the dump. So the payload is written unconditionally, for every matching tool call, in every session, on every repository the agent works in.

Why it matters

Three separate problems, in increasing order of severity.

  1. It writes into the repository under work, not into a state directory. PROJECT_ROOT is the git toplevel of the session's cwd, so the log materialises as logs/qa-automation.log inside a checkout. Whether that file is ignored depends entirely on the repository it lands in; nothing in the hook guarantees it. An agent that stages broadly commits it.

  2. The payload is the complete tool input and output. $JSON_INPUT is the raw PostToolUse JSON, which carries tool_input, tool_response, and the session/transcript identifiers the runtime attaches. That is a verbatim transcript of the agent's work, written to disk by a component whose stated job is running lint and typecheck.

  3. Anything that passed through a matching tool call is now in a file in a working tree. A framework whose own standards say never to emit a credential value in any output has a hook that transcribes tool payloads wholesale, unfiltered, into the repository. The two rules cannot both hold.

Evidence

Measured directly from the installed script at the line numbers quoted above. Independently observed in the field: an agent checkout accumulated logs/qa-automation.log containing that agent's session identifier, noticed by the agent itself while auditing its own working tree for stray files.

Suggested direction

Not prescriptive, but the shape seems clear:

  • Write to a state directory outside any checkout (${XDG_STATE_HOME:-$HOME/.local/state}/mosaic/qa/), keyed by session, never to PROJECT_ROOT.
  • Move the dump behind the scope filter, so an out-of-scope tool call logs nothing.
  • Log the extracted fields the handler actually needs (tool, file_path, verdict) rather than the raw payload. The full-JSON dump reads like a debugging aid that was never removed.
  • If a raw-payload mode is genuinely wanted for debugging, make it opt-in via an environment variable and say in the log header that it contains tool payloads.

A regression test that asserts no file is created under the repository root after a hook invocation would keep it fixed.

## What happens `tools/qa/qa-hook-stdin.sh` is installed as a Claude Code `PostToolUse` hook on `Edit|MultiEdit|Write`. Before it decides whether the tool call is even in scope, it appends the **entire hook payload** to a log file inside the repository being worked on: ``` 7 PROJECT_ROOT=$(git rev-parse --show-toplevel 2>/dev/null || pwd) 8 LOG_FILE="$PROJECT_ROOT/logs/qa-automation.log" ... 15 echo "[$(date ...)] Hook triggered with JSON:" >> "$LOG_FILE" 16 echo "$JSON_INPUT" >> "$LOG_FILE" ``` The scope filter — "skip non-JS/TS file" — is at line 49, thirty-three lines *after* the dump. So the payload is written unconditionally, for every matching tool call, in every session, on every repository the agent works in. ## Why it matters Three separate problems, in increasing order of severity. 1. **It writes into the repository under work, not into a state directory.** `PROJECT_ROOT` is the git toplevel of the session's cwd, so the log materialises as `logs/qa-automation.log` inside a checkout. Whether that file is ignored depends entirely on the repository it lands in; nothing in the hook guarantees it. An agent that stages broadly commits it. 2. **The payload is the complete tool input and output.** `$JSON_INPUT` is the raw PostToolUse JSON, which carries `tool_input`, `tool_response`, and the session/transcript identifiers the runtime attaches. That is a verbatim transcript of the agent's work, written to disk by a component whose stated job is running lint and typecheck. 3. **Anything that passed through a matching tool call is now in a file in a working tree.** A framework whose own standards say never to emit a credential value in any output has a hook that transcribes tool payloads wholesale, unfiltered, into the repository. The two rules cannot both hold. ## Evidence Measured directly from the installed script at the line numbers quoted above. Independently observed in the field: an agent checkout accumulated `logs/qa-automation.log` containing that agent's session identifier, noticed by the agent itself while auditing its own working tree for stray files. ## Suggested direction Not prescriptive, but the shape seems clear: - Write to a state directory outside any checkout (`${XDG_STATE_HOME:-$HOME/.local/state}/mosaic/qa/`), keyed by session, never to `PROJECT_ROOT`. - Move the dump behind the scope filter, so an out-of-scope tool call logs nothing. - Log the extracted fields the handler actually needs (`tool`, `file_path`, verdict) rather than the raw payload. The full-JSON dump reads like a debugging aid that was never removed. - If a raw-payload mode is genuinely wanted for debugging, make it opt-in via an environment variable and say in the log header that it contains tool payloads. A regression test that asserts no file is created under the repository root after a hook invocation would keep it fixed.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: mosaicstack/stack#1254