Designing a Hooks System for AI Agent CLIs: 4 Lifecycle Points That Matter

If you're building an AI agent that runs in the terminal, you'll eventually face a design question that sounds simple but isn't: how do you let users extend your tool without turning it into a plugin framework?

The answer that's emerging across tools like Claude Code, Aider, and Continue is lifecycle hooks—shell commands that fire at specific moments in your agent's execution. Not plugins. Not extensions. Just scripts.

This approach is elegant because it leverages what developers already know (bash, Python scripts, Node.js) and what CI/CD has proven for years: hooks are simple, debuggable, and powerful.

Here's how to design a hooks system that actually gets used.

The Four Lifecycle Points You Actually Need

Most CLIs over-engineer their hooks. They create 15 different events and users ignore 12 of them. The reality is that four lifecycle points cover 90% of real-world use cases:

1. Before Tool Execution (pre-tool or before-command)

This fires right before your agent runs a tool call—before it reads a file, executes bash, or makes an API request.

Why it matters: Users can enforce policies, log actions, or inject context. For example:

  • Block file writes to production config
  • Log every API call to an audit file
  • Set environment variables based on the current git branch

Implementation tip: Pass the tool name and arguments as environment variables. Make it easy to filter: if [ "$TOOL_NAME" = "Write" ]; then ...

2. After Tool Execution (post-tool or after-command)

This fires after a tool completes, with access to the result.

Why it matters: Users can validate outputs, trigger side effects, or update state:

  • Run linters after file edits
  • Sync changes to a remote server
  • Update a local knowledge base when new files are written

Implementation tip: Provide both TOOL_RESULT (the output) and TOOL_EXIT_CODE (success/failure). Let the hook decide whether to act on success, failure, or both.

3. On User Message Submit (pre-submit or user-prompt-submit-hook)

This fires when the user sends a message, before the agent processes it.

Why it matters: This is your preprocessing layer:

  • Auto-attach git status to every message
  • Inject TODO context from project management tools
  • Run quick checks ("are tests passing?") and append to the prompt

Real example: A developer working on a microservices project uses this hook to auto-include the current service's OpenAPI spec whenever they ask a question. The agent always has API context without being asked.

4. On Agent Response (post-response or agent-complete-hook)

This fires after the agent finishes its turn.

Why it matters: Users can process outputs, trigger notifications, or update dashboards:

  • Send a Slack notification when a long task completes
  • Extract code blocks and auto-save them to a scratch directory
  • Update time-tracking tools

Implementation tip: Provide the full response text and metadata (token count, model used, duration). This is where analytics hooks live.

Why Shell-Only Is a Feature, Not a Limitation

Here's a controversial take: don't support native plugin APIs. Only shell commands.

Why? Because:

  1. Shell scripts are universal. Every developer can write them. No SDK to learn, no versioning hell, no compilation step.

  2. They're debuggable. Users can run ./my-hook.sh independently. Try debugging a plugin loaded into your runtime.

  3. They're portable. Hooks work across languages. A Python user and a Node.js user can share hooks without translation.

  4. They force simplicity. If a hook needs to parse JSON, the user installs jq. If it needs HTTP, they use curl. This pushes complexity to standard tools, not your codebase.

CI/CD proved this model works. GitHub Actions, GitLab CI, and pre-commit hooks all use shell scripts as the extension point. Developers understand the model.

Configuration: Make It Obvious

Store hooks in your tool's config file, not a separate hooks directory. Example:

{
  "hooks": {
    "pre-tool": "./scripts/audit-tool.sh",
    "post-tool": "./scripts/lint-on-write.sh",
    "user-prompt-submit-hook": "./scripts/inject-context.sh",
    "post-response": "./scripts/notify-slack.sh"
  }
}

This makes hooks discoverable. A new team member sees the config and immediately understands what automation is in place.

What to Cut: The Hooks You Don't Need

When designing this, you'll be tempted to add:

  • on-startup / on-shutdown – Users can handle this with shell wrappers (my-hook.sh && agent-cli)
  • on-error – Covered by post-tool with exit codes
  • on-file-change – Use existing file watchers (watchman, entr, nodemon)
  • Per-tool hooks (on-write, on-read) – Too granular. Use pre-tool and filter by $TOOL_NAME

Every hook you add is maintenance debt. Four points is enough.

Real-World Example: Auto-Testing on Code Changes

Here's a post-tool hook that runs tests whenever the agent writes to a source file:

#!/bin/bash
# post-tool.sh

if [ "$TOOL_NAME" = "Write" ] || [ "$TOOL_NAME" = "Edit" ]; then
  file="$TOOL_ARG_FILE_PATH"
  if [[ "$file" == src/*.ts ]]; then
    echo "Running tests for $file..."
    npm test -- "$file.test.ts" || echo "Tests failed. Agent will see this output."
  fi
fi

The agent sees the test output in the tool result. If tests fail, it can fix them in the next turn. Zero manual intervention.

The Takeaway

If you're building an AI agent CLI, hooks are your extensibility layer. They're simpler than plugins, more powerful than config flags, and align with how developers already think.

Stick to four lifecycle points: before/after tool execution, on user message, and on agent response. Keep it shell-only. Put it in your main config file.

Your users will automate workflows you never imagined—and they'll do it without filing a feature request.