DEV Community

Aqsa Zafar
Aqsa Zafar

Posted on

Building a Terminal-Based AI Automation Pipeline with Claude Code Hooks + jq

When I started using Claude Code inside the terminal, I noticed something important.

Prompts are flexible. But flexibility also means variability.

If you want:

  • Predictable formatting
  • Structured logging
  • Validation before execution
  • Cleaned terminal output
  • Error detection

Prompts alone are not enough.

Hooks give you control.

In this post, I’ll show you how I combine:

  • post-tool-use hooks
  • jq for JSON parsing
  • A small Python script for output cleaning

The goal is simple: turn Claude Code into a predictable automation layer inside the terminal.

1. How Hooks Actually Work

Claude Code triggers lifecycle events such as:

  • pre-tool-use
  • post-tool-use
  • session-start
  • session-end
  • user-prompt-submit

When a hook is registered for one of these events, Claude passes a structured JSON payload to the hook via standard input (stdin).

That detail matters.

Your hook script reads the event JSON from stdin, processes it, and optionally prints modified output.

So every example below assumes:

Claude pipes event JSON into your hook command via stdin.

2. Inspecting the Hook Payload with jq

Every hook receives JSON like this:

{
  "tool_name": "bash",
  "tool_input": {
    "code": "ls -a",
    "description": "List all files including hidden ones"
  },
  "tool_response": {
    "stdout": "file1\nfile2\n..."
  }
}
Enter fullscreen mode Exit fullscreen mode

You rarely need the entire payload. You usually need specific fields.

Install jq:

macOS

brew install jq
Enter fullscreen mode Exit fullscreen mode

Ubuntu

sudo apt install jq
Enter fullscreen mode Exit fullscreen mode

Verify:

jq --version
Enter fullscreen mode Exit fullscreen mode

Extract Specific Fields

Since Claude pipes JSON into stdin, your hook command can read directly from it.

Extract command:

jq -r '.tool_input.code'
Enter fullscreen mode Exit fullscreen mode

Extract description: ""

jq -r '.tool_input.description'
Enter fullscreen mode Exit fullscreen mode

Combine both:

jq -r '"\(.tool_input.code) - \(.tool_input.description)"'
Enter fullscreen mode Exit fullscreen mode

The -r flag prints raw output (no quotes).

This is the foundation for logging and enforcement.

3. Logging Every Bash Command (post-tool-use Hook)

Now let’s make it real.

Goal: log every Bash command Claude executes.

Why post-tool-use?

Because I want to log what actually ran, not what was proposed.

Minimal Hook Configuration Example

Inside your Claude settings (user-level or project-level), the hook entry looks like this conceptually:

{
  "event": "post-tool-use",
  "matcher": {
    "tool": "bash"
  },
  "command": "jq -r '\"\\(.tool_input.code) - \\(.tool_input.description)\"' >> ~/.claude/bash-command-log.txt"
}
Enter fullscreen mode Exit fullscreen mode

What happens:

  1. Bash tool runs
  2. Claude triggers post-tool-use
  3. JSON payload is piped into jq
  4. Extracted values are appended to log file

Now your terminal activity becomes traceable.

This is useful for:

  • Debugging
  • Workflow auditing
  • Understanding automation patterns
  • Reviewing repeated commands

4. Cleaning Tool Output with Python (Post-Processing Layer)

Now let’s improve readability.

Large outputs like:

ls -a
Enter fullscreen mode Exit fullscreen mode

can flood the interface.

Instead of manually scanning it, intercept and clean it.


Hook Command (Conceptual)

{
  "event": "post-tool-use",
  "command": "python3 ~/.claude/hooks/clean_validate_hook.py"
}
Enter fullscreen mode Exit fullscreen mode

Claude pipes JSON into that script via stdin.

Python Script: clean_validate_hook.py

import sys
import json
import re

try:
    data = json.load(sys.stdin)
except json.JSONDecodeError:
    sys.exit(0)

tool_response = data.get("tool_response") or {}
stdout = tool_response.get("stdout") or ""

if not stdout.strip():
    sys.exit(0)

# Remove common ANSI color codes
clean_output = re.sub(r'\x1b\[[0-9;]*m', '', stdout)

lines = clean_output.splitlines()

# Limit preview length
preview_limit = 10
preview = "\n".join(lines[:preview_limit])

print(preview)
Enter fullscreen mode Exit fullscreen mode

What this does:

  • Reads event JSON from stdin
  • Extracts stdout safely
  • Removes common ANSI color codes
  • Limits output to first 10 lines
  • Prints a clean preview

You can extend it to:

  • Detect errors
  • Highlight warnings
  • Add timestamps
  • Save summaries to a log file

5. What This Pipeline Actually Gives You

With just:

  • Hooks
  • jq
  • A small Python script

You now have:

  • Structured command logging
  • Controlled output formatting
  • Automatic enforcement layer
  • Repeatable terminal behavior

Claude stops being just a prompt interface. It becomes programmable middleware inside your workflow.

6. Security Considerations

Hooks run with your environment’s permissions.

That means:

  • Validate input
  • Avoid exposing secrets
  • Keep scripts under version control
  • Review commands before registering them

Treat hooks like infrastructure, not shortcuts.

7. Where This Can Break

Be realistic:

  • If payload structure changes, jq filters must update
  • If tool output changes format, parsing may fail
  • Over-filtering can hide useful debugging data

Keep hooks small. Keep them explicit.

Final Thoughts

Once you start intercepting tool events:

  • You stop reacting to output
  • You start shaping it

That’s the difference between using a tool and engineering a workflow.

If you want to see the full structured walkthrough, including:

  • Hook setup inside Claude
  • jq parsing patterns
  • Advanced matchers
  • Subagents
  • MCP integrations
  • GitHub Actions automation
  • Plugin development

I’ve documented everything step by step here:

👉 Claude Code Course — Step-by-Step Guide from CLI to Real Workflows

If you’re already using Claude Code in the terminal, I’m curious:

Are you still relying on prompts, or are you intercepting the pipeline?

Because once you control lifecycle events, your terminal becomes predictable.

Happy Learning!

Top comments (0)