Automation: Hooks

Hooks are a powerful automation feature that let you run custom commands and scripts in response to events in Lectic’s lifecycle. Use them for logging, notifications, post‑processing, or integrating with other tools and workflows.

Hooks are defined in your YAML configuration under the hooks key, per-tool in the hooks key of a tool specification, or per-interlocutor in the hooks key of an interlocutor specification.

Hook configuration

A hook has three fields:

  • on: A single event name or a list of event names to listen for.
  • do: The command or inline script to run when the event fires.
  • inline: (Optional) A boolean. If true, the standard output of the command is captured and injected into the conversation. Defaults to false. Only applicable to assistant_message and user_message.
hooks:
  - on: [assistant_message, user_message]
    do: ./log-activity.sh

If do contains multiple lines, it is treated as a script and must begin with a shebang (e.g., #!/bin/bash). If it is a single line, it is treated as a command. Commands are executed directly (not through a shell), so shell features like command substitution will not work.

Hook commands run synchronously. By default, their stdout, stderr, and exit status are ignored by Lectic. However, if you set inline: true, the standard output is captured and added to the conversation.

  • For user_message events, the output is injected as context for the LLM before it generates a response. It also appears at the top of the assistant’s response block.
  • For assistant_message events, the output is appended to the end of the assistant’s response block. This will trigger another reply from the assistant, so be careful to only fire an inline hook when you want the assisant to generate more content.

Available events and environment

Lectic emits three hook events. When an event fires, the hook process receives its context as environment variables. No positional arguments are passed. However, the hook may receive content via standard input.

  • user_message
    • Environment:
      • USER_MESSAGE: The text of the most recent user message.
      • Standard Lectic variables like LECTIC_FILE, LECTIC_CONFIG, LECTIC_DATA, LECTIC_CACHE, LECTIC_STATE, and LECTIC_TEMP are also set when available.
    • When: Just before the request is sent to the LLM provider.
  • assistant_message
    • Standard Input: The raw markdown text of the conversation body up to this point.
    • Environment:
      • ASSISTANT_MESSAGE: The full text of the assistant’s response that was just produced.
      • LECTIC_INTERLOCUTOR: The name of the interlocutor who spoke.
      • LECTIC_MODEL: The model of the interlocutor who spoke.
      • TOOL_USE_DONE: Set to 1 when the assistant has finished using tools and is ready to conclude. Not set if there are pending tool calls. This lets inline hooks decide whether to inject follow-up content only when all work is complete.
      • LECTIC_TOKEN_USAGE_INPUT: Count of input tokens used for this turn.
      • LECTIC_TOKEN_USAGE_OUTPUT: Count of output tokens used for this turn.
      • LECTIC_TOKEN_USAGE_TOTAL: Total tokens used for this turn.
      • LECTIC_LOOP_COUNT: How many times the tool calling loop has run (0-indexed).
      • LECTIC_FINAL_PASS_COUNT: How many times the assistant has finished work but was kept alive by an inline hook.
      • Standard Lectic variables as above.
    • When: Immediately after the assistant’s message is streamed.
  • tool_use_pre
    • Environment:
      • TOOL_NAME: The name of the tool being called.
      • TOOL_ARGS: A JSON string containing the tool arguments.
      • Standard Lectic variables as above.
    • When: After tool parameters are collected but before execution.
    • Behavior: If the hook exits with a non-zero status code, the tool call is blocked, and the LLM receives a “permission denied” error.
  • error
    • Environment:
      • ERROR_MESSAGE: A descriptive error message.
      • Standard Lectic variables as above.
    • When: Whenever an uncaught error is encountered.

Hook headers and attributes

Hooks can pass metadata back to Lectic by including headers at the very beginning of their output. Headers follow the format LECTIC:KEY:VALUE and must appear before any other content. The headers are stripped from the visible output and stored as attributes on the inline block.

#!/usr/bin/env bash
echo "LECTIC:final:true"
echo ""
echo "System check complete. One issue found."

Currently, only one header affects control flow:

  • final: When an inline hook generates output, Lectic normally continues the tool calling loop so that the assistant can see and respond to the new information. If the final header is present, Lectic prevents this extra pass, allowing the conversation turn to end immediately (unless the assistant explicitly called a tool).

Example: Human-in-the-loop tool confirmation

This example uses tool_use_pre to require confirmation before any tool execution. It uses zenity to show a dialog box with the tool name and arguments.

hooks:
  - on: tool_use_pre
    do: |
      #!/usr/bin/env bash
      # Display a confirmation dialog
      zenity --question \
             --title="Allow Tool Use?" \
             --text="Tool: $TOOL_NAME\nArgs: $TOOL_ARGS"
      # Zenity exits with 0 for Yes/OK and 1 for No/Cancel
      exit $?

This example persists every user and assistant message to an SQLite database located in your Lectic data directory. You can later query this for personal memory, project history, or analytics.

Configuration:

hooks:
  - on: [user_message, assistant_message]
    do: |
      #!/usr/bin/env bash
      set -euo pipefail
      DB_ROOT="${LECTIC_DATA:-$HOME/.local/share/lectic}"
      DB_PATH="${DB_ROOT}/memory.sqlite3"
      mkdir -p "${DB_ROOT}"

      # Determine role and text from available variables
      if [[ -n "${ASSISTANT_MESSAGE:-}" ]]; then
        ROLE="assistant"
        TEXT="$ASSISTANT_MESSAGE"
      else
        ROLE="user"
        TEXT="${USER_MESSAGE:-}"
      fi

      # Basic sanitizer for single quotes for SQL literal
      esc_sq() { printf %s "$1" | sed "s/'/''/g"; }

      TS=$(date -Is)
      FILE_PATH="${LECTIC_FILE:-}"
      NAME="${LECTIC_INTERLOCUTOR:-}"

      sqlite3 "$DB_PATH" <<SQL
      CREATE TABLE IF NOT EXISTS memory (
        id INTEGER PRIMARY KEY,
        ts TEXT NOT NULL,
        role TEXT NOT NULL,
        interlocutor TEXT,
        file TEXT,
        text TEXT NOT NULL
      );
      INSERT INTO memory(ts, role, interlocutor, file, text)
      VALUES ('${TS}', '${ROLE}', '$(esc_sq "$NAME")',
              '$(esc_sq "$FILE_PATH")', '$(esc_sq "$TEXT")');
      SQL

Notes:

  • Requires the sqlite3 command-line tool to be installed and on your PATH.
  • The hook inspects which variable is set to decide whether the event was a user or assistant message.
  • LECTIC_FILE is populated when using -f/-i and may be empty when streaming from stdin.
  • Adjust the table schema to suit your use case.

Example: Injecting context

This example automatically runs date before every user message and injects the output into the context. This allows the LLM to always know the date and time without you needing to run :cmd[date]

hooks:
  - on: user_message
    inline: true
    do: 
      #!/usr/bin/env bash
      echo "<date-and-time>"
      date
      echo "</date-and-time>"

Example: Notification when work completes

This example sends a desktop notification when the assistant finishes a tool-use workflow. The hook checks TOOL_USE_DONE so you only get notified once the work is actually done, not after each intermediate step.

hooks:
  - on: assistant_message
    do: |
      #!/usr/bin/env bash
      if [[ "${TOOL_USE_DONE:-}" == "1" ]]; then
        notify-send "Lectic" "Assistant finished working"
      fi

This is especially useful for long-running agentic tasks where you want to step away and be alerted when the assistant is done.