Tutorials Search / Native Mac IDE / Add custom hooks
πŸ“ Written ● Intermediate Updated 2026-05-13

How do I add custom hooks to LingCode?

TL;DR: Edit ~/.claude/settings.json (or .claude/settings.json in a project) and add a hooks entry mapping events (PreToolUse, UserPromptSubmit, SessionStart, Stop) to shell commands. The harness runs the command, not the agent β€” useful for guardrails and automation.

A hook is a shell command LingCode runs at a specific point in the agent's lifecycle β€” before a tool call, after a prompt is submitted, when a session ends. Hooks enforce policy: not "please don't do X" written in a CLAUDE.md, but actual code that says no.

There are two ways to influence what the agent does. One is to tell it: in a CLAUDE.md, in a skill, in the prompt itself. Telling works most of the time. It also fails sometimes, because the agent is a model and models are probabilistic. For things that absolutely must happen β€” or must never happen β€” telling isn't enough. The rule you wrote down can be ignored under pressure, missed entirely on a fresh chat, or quietly violated when the agent decides the situation is different from the one you described.

The other way is to enforce it: code that runs as the agent works, around its actions. A PreToolUse hook fires before any tool call and can veto it. A UserPromptSubmit hook fires when you send a message and can log, modify, or block it. A PostToolUse hook fires after a successful tool call. A SessionEnd hook fires when the chat closes. Each hook is a shell command β€” your code β€” that gets context as environment variables and decides what happens next by its exit code and output.

Use hooks when the wrong outcome is expensive enough that a probabilistic safeguard isn't enough. "Never let the agent push to main." "Always run my linter before committing." "Log every shell command the agent ran today for audit." These are policy questions, not vibes β€” and hooks are how you make them code.

What you'll learn

Step 1: The four events

1

What each one does

  • UserPromptSubmit β€” fires every time you press send. Gets the prompt text. Can log it, modify it, or block the submission. Common use: redact secrets the user accidentally typed.
  • PreToolUse β€” fires before any tool call. Gets the tool name and arguments. Can approve, log, or veto. Common use: refuse shell commands that match a dangerous pattern.
  • PostToolUse β€” fires after a successful tool call. Gets the tool name, arguments, and result. Common use: write an audit log; trigger a downstream notification.
  • SessionEnd β€” fires when the chat is closed or the agent exits. Common use: cleanup, rollup metrics, send a summary.

The events are the same across both the Mac app's chat and the CLI. A hook you write works in either context.

Step 2: The hooks.json format

2

A JSON file with one entry per hook

Create .claude/hooks/hooks.json in your project (or ~/.claude/hooks/hooks.json user-wide):

{
  "PreToolUse": [
    {
      "matcher": "shell",
      "command": "./.claude/hooks/block-dangerous-shell.sh"
    }
  ],
  "UserPromptSubmit": [
    {
      "command": "./.claude/hooks/log-prompt.sh"
    }
  ]
}

Each event takes an array of hook entries. Each entry has at least a command (the shell command to run) and optionally a matcher (only fire for tool calls matching this pattern). The command can be a script path, an inline expression, anything your shell would accept.

Step 3: What the hook receives

3

Env vars with everything the agent knew

When the hook runs, LingCode sets a set of environment variables prefixed LINGCODE_ (and aliased as CLAUDE_ for Claude Code compatibility). The common ones:

  • LINGCODE_EVENT β€” the event name (PreToolUse, etc.)
  • LINGCODE_TOOL_NAME β€” for tool-related events, the name of the tool
  • LINGCODE_TOOL_ARGS β€” JSON of the tool's arguments
  • LINGCODE_PROMPT β€” for UserPromptSubmit, the raw prompt text
  • LINGCODE_SESSION_ID β€” a stable ID for the current conversation, useful for log correlation
  • LINGCODE_CWD β€” the project's working directory

Your hook reads these, does its work, and exits. Standard streams: stdin is empty, stdout is captured for context, stderr surfaces in the chat panel if the hook blocks.

Step 4: Exit code 0 means allow; 1 means block

4

The simplest possible contract

A hook exits 0 to allow the event to continue, non-zero to block it. If a PreToolUse hook exits 1, the tool call is refused; the agent sees the refusal and adapts. If a UserPromptSubmit hook exits 1, the message is not sent. Stderr from a blocking hook becomes a message to the agent explaining why β€” write a clear one-line reason there.

For modifications (rewriting a prompt, redacting an argument), the hook writes the new value to stdout and exits 0. LingCode replaces the original with the hook's stdout. This is how a "redact secrets" hook works.

Step 5: A concrete example

5

Block git push --force to main

Create .claude/hooks/block-force-push.sh:

#!/usr/bin/env bash
set -euo pipefail

if [[ "${LINGCODE_TOOL_NAME}" == "shell" ]]; then
  cmd=$(echo "$LINGCODE_TOOL_ARGS" | jq -r '.command // ""')
  if echo "$cmd" | grep -qE 'git push (-f|--force).*main\b'; then
    echo "Refused: --force push to main is not allowed. Open a PR instead." >&2
    exit 1
  fi
fi
exit 0

Then in .claude/hooks/hooks.json:

{
  "PreToolUse": [
    {
      "matcher": "shell",
      "command": "./.claude/hooks/block-force-push.sh"
    }
  ]
}

Make the script executable (chmod +x). From now on, any agent attempt to force-push to main is refused at the tool boundary β€” the agent sees the stderr message and tries a different approach. There's no way to talk it out of this rule; it's code now.

Sovereign over permission modes: A hook that blocks a tool blocks it in all permission modes. Setting acceptEdits doesn't bypass hooks. This is intentional β€” hooks are policy, modes are convenience.

Step 6: Three places hooks can live

6

Project, user-wide, or plugin-bundled

  • Project-local: .claude/hooks/ at the project root. Tracked in git so the whole team gets the rule.
  • User-wide: ~/.claude/hooks/. Right for personal habits (e.g. "always lint before committing across all my projects").
  • Plugin-bundled: in a plugin's hooks/ directory. Lets you distribute hooks reusably β€” but installing such a plugin triggers a consent prompt, because hooks can run shell.
Don't fail open. If your hook script has a bug and exits non-zero on every event, you've effectively blocked the agent. Test hook scripts in isolation (run the .sh file with the env vars set manually) before deploying. A failing hook is worse than a missing hook.

What's next