Push To Display
← Back to blog

Claude Code Hooks — Push AI Agent Activity to a Physical Display

Updated September 2026: this guide was rewritten against the current Claude Code hooks reference. Hook input arrives as JSON on stdin — not environment variables — and the events are PostToolUse, PostToolUseFailure, and SessionEnd. Older versions of this page used event names and env vars that Claude Code never had; those scripts will not fire.

Claude Code works autonomously. You give it a task, walk away, come back 20 minutes later — and then spend 5 minutes scrolling terminal output to figure out what happened.

For teams running multiple Claude Code sessions across different repos, the visibility problem compounds. Nobody knows what the AI agents are doing until someone checks manually.

What if every Claude Code session pushed its activity to a screen on your desk — task completions, failures, progress — the same way a CI dashboard shows pipeline status?

This tutorial wires Claude Code's hook system into PushToDisplay so agent activity appears on any device in real time.

What you'll build

A 4-panel display where each panel shows a different aspect of Claude Code's activity:

PanelHook eventContent
Panel 1PostToolUse (Write|Edit)Completed file changes, with the file name
Panel 2PostToolUseFailureRed alert with the first line of the error
Panel 3PostToolUse (all tools)Current activity, throttled to one update every 3 s
Panel 4SessionEndProject, exit reason, and short session ID

Your agent works. Your screen updates. You glance over and know where things stand.

Prerequisites

  1. Claude Code installed and working (claude CLI available)
  2. PushToDisplay CLI installed and authenticated: npm install -g pushtodisplay, then pushtodisplay auth login (or pushtodisplay auth login --api-key pt_...)
  3. jq installed — the hook scripts parse JSON with it (brew install jq, sudo apt install jq)
  4. Board layout set to 2×2 Grid in the app (Settings → Layout → 2×2 Grid)
  5. A default board set in the app, or a board ID you'll add to the payloads below

How Claude Code hooks work

Hooks are shell commands that run at specific points in a session's lifecycle. You configure them in your project's .claude/settings.json or globally in ~/.claude/settings.json.

Three things are worth knowing before you write a script:

  1. The config is event → matcher groups → handlers. Each event holds an array of groups; each group has an optional matcher and an array of hooks.
  2. Command handlers receive the event as JSON on stdin. Not environment variables. Your script reads stdin and parses fields like tool_name, tool_input, and — for failures — error.
  3. Hooks can block actions, but ours don't. We only observe and forward; the scripts always exit 0.

The events used here:

EventFires when
PostToolUseA tool call completes successfully
PostToolUseFailureA tool call starts and fails
SessionEndThe session ends (exit, /clear, logout, and so on)

The full event list is in the official reference.

The config

Create ~/.claude/hooks/ and add this to ~/.claude/settings.json (or the project-level .claude/settings.json):

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write|Edit",
        "hooks": [
          { "type": "command", "command": "$HOME/.claude/hooks/post-tool.sh" }
        ]
      },
      {
        "hooks": [
          { "type": "command", "command": "$HOME/.claude/hooks/progress.sh" }
        ]
      }
    ],
    "PostToolUseFailure": [
      {
        "hooks": [
          { "type": "command", "command": "$HOME/.claude/hooks/on-failure.sh" }
        ]
      }
    ],
    "SessionEnd": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "$HOME/.claude/hooks/session-end.sh",
            "timeout": 10
          }
        ]
      }
    ]
  }
}

Two PostToolUse groups: the first matches Write|Edit only (the completion panel), the second has no matcher, so it runs after every successful tool call (the progress panel).

SessionEnd has a 1.5-second default timeout. This hook shells out to the CLI, so the config above raises it to 10 seconds.

Save the four scripts below into ~/.claude/hooks/, then make them executable:

chmod +x ~/.claude/hooks/*.sh

Hook 1: completed file changes → Panel 1

When Claude Code writes or edits a file, push the tool name and file to Panel 1.

Create ~/.claude/hooks/post-tool.sh:

#!/bin/bash
# ~/.claude/hooks/post-tool.sh — completed file changes → Panel 1

INPUT=$(cat)
TOOL=$(echo "$INPUT" | jq -r '.tool_name')
FILE=$(echo "$INPUT" | jq -r '.tool_input.file_path // "unknown file"')
NAME=$(basename "$FILE")

jq -n \
  --arg tool "$TOOL" \
  --arg file "$NAME" \
  --arg time "$(date '+%H:%M:%S')" \
  '{
    panelId: 1,
    background: "#0D7A3E",
    fullPanel: true,
    blocks: [
      { text: ("✓ " + $tool), size: "large", weight: "bold", color: "#FFFFFF" },
      { text: $file, size: "medium", color: "#E2E8F0" },
      { text: $time, size: "small", color: "#CBD5E1" }
    ]
  }' | pushtodisplay send --stdin

A few details that make this reliable:

  • jq -n --arg ... builds the JSON payload, so file names with quotes or spaces can't break the request.
  • fullPanel: true makes the message fill the panel.
  • Colors are 6-digit hex (#RRGGBB). The API rejects 8-digit alpha values.
  • pushtodisplay send --stdin accepts the same JSON body as the HTTP API.

Hook 2: failed tool calls → Panel 2

PostToolUseFailure fires when a tool starts and throws — a failed test run, a rejected command, an MCP tool returning an error. It carries the same tool_name/tool_input fields as PostToolUse, plus an error string.

Create ~/.claude/hooks/on-failure.sh:

#!/bin/bash
# ~/.claude/hooks/on-failure.sh — failed tool calls → Panel 2

INPUT=$(cat)
TOOL=$(echo "$INPUT" | jq -r '.tool_name')
ERROR=$(echo "$INPUT" | jq -r '.error // "Unknown failure"')
SUMMARY=$(echo "$ERROR" | head -n 1 | head -c 120)

jq -n \
  --arg tool "$TOOL" \
  --arg summary "$SUMMARY" \
  '{
    panelId: 2,
    background: "#C0392B",
    fullPanel: true,
    blocks: [
      { text: "⚠ Agent blocked", size: "large", weight: "bold", color: "#FFFFFF" },
      { text: $tool, size: "medium", color: "#E2E8F0" },
      { text: $summary, size: "small", color: "#CBD5E1" }
    ]
  }' | pushtodisplay send --stdin

For Bash failures the first line is usually Exit code N; for other tools it's whatever the tool reported. Keep the panel to the first line — the full error belongs in your terminal or logs.

One thing this hook does not catch: tool calls rejected before execution (unknown tool, schema validation, permission denials). Those fire other events, per the reference.

Hook 3: current activity → Panel 3

The second PostToolUse group runs after every successful tool call — reads, searches, commands, everything. That's a lot of updates, so the script throttles itself to one push every 3 seconds.

Create ~/.claude/hooks/progress.sh:

#!/bin/bash
# ~/.claude/hooks/progress.sh — current activity → Panel 3

INPUT=$(cat)

# Throttle: at most one update every 3 seconds
LAST="$HOME/.claude/ptd-last-push"
NOW=$(date +%s)
if [ -f "$LAST" ] && [ $(( NOW - $(cat "$LAST") )) -lt 3 ]; then
  exit 0
fi
echo "$NOW" > "$LAST"

TOOL=$(echo "$INPUT" | jq -r '.tool_name')
HINT=$(echo "$INPUT" | jq -r '(.tool_input.file_path // .tool_input.command // .tool_input.pattern // "") | tostring | .[0:80]')

jq -n \
  --arg tool "$TOOL" \
  --arg hint "$HINT" \
  --arg time "$(date '+%H:%M')" \
  '{
    panelId: 3,
    background: "#2C3E50",
    fullPanel: true,
    blocks: [
      { text: "Working...", size: "large", weight: "bold", color: "#FFFFFF" },
      { text: $tool, size: "medium", color: "#E2E8F0" },
      { text: ($hint + "  •  " + $time), size: "small", color: "#CBD5E1" }
    ]
  }' | pushtodisplay send --stdin

Glance at the screen: if Panel 3 still says "Working..." with a recent timestamp, the agent is active. If the timestamp is stale, the session may be waiting on you.

The throttle matters for more than noise: even paid PushToDisplay tiers cap sustained message rates, so a hook on every tool call without a filter will collect 429 responses. See Rate limits and quotas.

Hook 4: session summary → Panel 4

When the session ends, push a final summary.

Create ~/.claude/hooks/session-end.sh:

#!/bin/bash
# ~/.claude/hooks/session-end.sh — session summary → Panel 4

INPUT=$(cat)
REASON=$(echo "$INPUT" | jq -r '.reason // "other"')
PROJECT=$(basename "$(echo "$INPUT" | jq -r '.cwd')")
SESSION=$(echo "$INPUT" | jq -r '.session_id' | cut -c1-8)

jq -n \
  --arg project "$PROJECT" \
  --arg reason "$REASON" \
  --arg session "$SESSION" \
  --arg time "$(date '+%H:%M')" \
  '{
    panelId: 4,
    background: "#1A5276",
    fullPanel: true,
    blocks: [
      { text: "Session ended", size: "large", weight: "bold", color: "#FFFFFF" },
      { text: $project, size: "medium", color: "#E2E8F0" },
      { text: ($reason + "  •  " + $session + "  •  " + $time), size: "small", color: "#CBD5E1" }
    ]
  }' | pushtodisplay send --stdin

reason tells you how the session ended: clear, resume, logout, prompt_input_exit, or other.

Alternative: call the API with curl

If you'd rather not install the CLI, build the same payload with curl:

curl -s -X POST https://api.pushtodisplay.com/v1/updates \
  -H "Content-Type: application/json" \
  -H "X-Api-Key: $PTD_API_KEY" \
  -d '{
    "panelId": 1,
    "background": "#0D7A3E",
    "fullPanel": true,
    "blocks": [
      { "text": "Task complete", "size": "large", "weight": "bold", "color": "#FFFFFF" }
    ]
  }'

Set PTD_API_KEY in your shell once, and the same script works from any hook event.

Team setup: one board per developer

The four panels above are designed for a single board. For teams, the cleanest setup is one board per developer — each person signs the CLI into their own account, and the hook scripts target their default board without any shared config. Mount the boards side by side, or give each developer their own display.

If you'd rather share one board, change the panelId values in the payloads so updates don't overwrite each other, and accept that each panel shows whichever session updated it last.

Consolidate AI agent output with everything else

The point isn't just Claude Code visibility — it's putting agent activity next to your other signals:

  • Panel 1: GitHub Actions build and test results (from the CI/CD dashboard setup)
  • Panel 2: Claude Code failures and blocked states
  • Panel 3: Current Claude Code activity
  • Panel 4: Deploy status from Vercel, AWS, or your CD pipeline

One screen: CI, CD, and AI agents.

Get started

  1. Install the CLI and sign in: npm install -g pushtodisplay && pushtodisplay auth login
  2. Install jq and create ~/.claude/hooks/
  3. Save the four scripts and run chmod +x ~/.claude/hooks/*.sh
  4. Add the config to ~/.claude/settings.json
  5. Start a Claude Code session and watch the panels update

Your AI agent's work — visible at a glance, without scrolling a terminal.


Related: GitHub Actions Dashboard tutorial · CLI docs · HTTP API reference · MCP Server for Claude Code