Gemini CLI Hooks Reference

Updated by

Independent reference, not affiliated with Google. Tables are generated from the Gemini CLI source at v0.62.0 (v0.62.0). Official documentation: geminicli.com.

Gemini CLI runs hooks on 11 lifecycle events, from BeforeTool to BeforeToolSelection. A hook is a shell command that receives a JSON payload on stdin and can answer with JSON on stdout to block, rewrite or annotate what happens next. Hooks live in the hooks block of settings.json or in an extension's hooks/hooks.json.

Which hook events does Gemini CLI support?#

Eventmatcher compared againstWhat stdout JSON can doSeveral hooks combineEvent-specific input
BeforeTooltool name (regex, unanchored)block the call (decision block/deny), force a confirmation (decision ask), rewrite tool_input, stop the agent (continue: false)any block wins; messages and context joinedtool_name, tool_input, mcp_context, original_request_name
AfterTooltool name (regex, unanchored)replace the result (decision block/deny), add context, chain a tool (tailToolCallRequest), stop the agent (continue: false)any block wins; messages and context joinedtool_name, tool_input, tool_response, mcp_context, original_request_name
BeforeAgentignoredblock the turn (decision block/deny), add context, stop the agent (continue: false)any block wins; messages and context joinedprompt
Notificationignoredobserve only (systemMessage, suppressOutput)later hook's fields replace earlier onesnotification_type, message, details
AfterAgentignoredsend reason back as a new prompt (decision block/deny), clear context (clearContext), stop the agent (continue: false)any block wins; messages and context joinedprompt, prompt_response, stop_hook_active
SessionStartsource (exact match)observe only (systemMessage, suppressOutput)any block wins; messages and context joinedsource
SessionEndreason (exact match)observe only (systemMessage, suppressOutput)later hook's fields replace earlier onesreason
PreCompresstrigger (exact match)observe only (systemMessage, suppressOutput)later hook's fields replace earlier onestrigger
BeforeModelignoredblock the call, optionally with a synthetic llm_response, rewrite llm_request, stop the agent (continue: false)later hook's fields replace earlier onesllm_request
AfterModelignoredreplace the response chunk (llm_response), block (decision block/deny), stop the agent (continue: false)later hook's fields replace earlier onesllm_request, llm_response
BeforeToolSelectionignoredrestrict tools (toolConfig mode, allowedFunctionNames)NONE wins, else ANY, else AUTO; function names unionedllm_request

Three events can rewrite model traffic rather than tool calls: BeforeModel edits the request or answers it with a synthetic response, AfterModel replaces each streamed response chunk, and BeforeToolSelection narrows which functions the model may call. When several hooks match one event they run in parallel and their outputs are merged as shown in the last column: for tool and agent events a single block from any hook blocks.

AfterAgent works like Claude Code's Stop hook: "decision": "block" sends the reason back to the model as a new prompt, and the retried turn arrives with stop_hook_active: true so the hook can avoid looping forever.

Where are hooks configured in settings.json?#

Hooks sit under a top-level hooks key, one array per event name. Each entry has an optional matcher and a hooks array of commands:

{
  "hooks": {
    "BeforeTool": [
      {
        "matcher": "run_shell_command|write_file",
        "hooks": [
          {
            "type": "command",
            "name": "guard-shell",
            "command": "$GEMINI_PROJECT_DIR/.gemini/hooks/guard.sh",
            "timeout": 30000
          }
        ]
      }
    ]
  }
}
KeyWhereTypeNotes
matcherdefinitionstringEmpty or * matches everything. Tool events: passed to new RegExp() and tested against the tool name, unanchored, so write the pattern without /slashes/; an invalid regex falls back to exact match. Trigger events: exact string match.
sequentialdefinitionbooleantrue runs every matching hook for the event one after another, each seeing the previous hook's changes; default is parallel
typehook"command"Only command hooks can be configured in settings.json.
commandhookstringShell command to execute. Receives JSON input via stdin and returns JSON output via stdout.
namehookstringUnique identifier for the hook.
descriptionhookstringA description of the hook.
timeouthooknumberMilliseconds. Default 60000 (60 s); on expiry SIGTERM, then SIGKILL 5 s later.
envhookRecord<string, string>Extra environment variables for this command.

The same hooks object can go in ~/.gemini/settings.json and in a project's .gemini/settings.json. The per-event arrays are concatenated across files, so user and project hooks both run. An extension ships the identical shape in hooks/hooks.json under a top-level "hooks" key.

A separate hooksConfig block switches the system on and off:

KeyTypeDefaultNotes
hooksConfig.enabledbooleantrueCanonical toggle for the hooks system. When disabled, no hooks will be executed.
hooksConfig.disabledstring[][]List of hook names (commands) that should be disabled. Hooks in this list will not execute even if configured.
hooksConfig.notificationsbooleantrueShow visual indicators when hooks are executing.

Hooks from settings.json run only in trusted folders; in an untrusted folder they are skipped. The first time a trusted project's .gemini/settings.json contains hooks you have not seen, Gemini CLI prints a warning listing them, records them in ~/.gemini/trusted_hooks.json, and runs them. It does not ask for approval first.

How does a Gemini CLI hook block a tool call?#

JSON on stdout decides first. {"decision": "deny", "reason": "..."} (or "block") from a BeforeTool hook stops the call, and the model receives Tool execution blocked: <reason>. "decision": "ask" replaces the policy engine's verdict with a confirmation prompt (a policy deny included), hookSpecificOutput.tool_input replaces the arguments, and "continue": false stops the whole agent loop with stopReason.

When the output is not JSON, Gemini CLI converts the text (stdout, or stderr when stdout is empty) by exit code:

Exit codeNon-JSON output (stdout, else stderr) becomesCounted as
0decision allow, text shown as systemMessagesuccess
1decision allow, systemMessage "Warning: <text>"failure (warning, not blocking)
2 or higherdecision deny, text as reasonfailure

Two consequences for plain-text hooks: exit 1 never blocks, it only shows a warning, and any exit of 2 or above blocks only when the hook printed some text to use as the reason. JSON output is honored whatever the exit code. A hook that exits 2 silently is logged as failed and the tool call proceeds. Any non-zero exit, timeout or spawn error shows "Hook(s) [name] failed for event ..." in the UI.

Every field below works on every event; hookSpecificOutput carries the event-specific fields listed further down.

FieldTypeNotes
continueboolean—
stopReasonstring—
suppressOutputboolean—
systemMessagestring—
decisionHookDecision—
reasonstring—
hookSpecificOutputRecord<string, unknown>—

What environment does a hook command get?#

The command runs through the platform shell with the session's working directory as its cwd, the sanitized parent environment, the hook's own env, and these variables:

VariableValueAlso substituted in command text
GEMINI_PROJECT_DIRsession working directoryyes
GEMINI_PLANS_DIRplans directoryyes
GEMINI_CWDsession working directoryyes
GEMINI_SESSION_IDsession idyes
CLAUDE_PROJECT_DIRsession working directoryyes

Occurrences of $GEMINI_PROJECT_DIR, $GEMINI_CWD, $GEMINI_PLANS_DIR, $GEMINI_SESSION_ID and $CLAUDE_PROJECT_DIR in the command string are replaced with shell-escaped values before the shell sees them.

How do Gemini CLI hooks compare to Claude Code hooks?#

The model is the same (JSON on stdin, JSON on stdout, matcher plus hooks array), but the names differ and so does the timeout unit. gemini hooks migrate --from-claude reads .claude/settings.local.json (or .claude/settings.json when there is no local file), renames events and tool names in matchers, swaps $CLAUDE_PROJECT_DIR for $GEMINI_PROJECT_DIR, and writes the result to the project's .gemini/settings.json.

gemini hooks migrate --from-claude copies timeout unchanged. Claude Code timeouts are seconds and Gemini CLI reads milliseconds, so multiply migrated values by 1000.
Claude Code eventGemini CLI event
PreToolUseBeforeTool
PostToolUseAfterTool
UserPromptSubmitBeforeAgent
StopAfterAgent
SubAgentStopAfterAgent
SessionStartSessionStart
SessionEndSessionEnd
PreCompactPreCompress
NotificationNotification
Claude Code toolGemini CLI tool
Editreplace
Bashrun_shell_command
Readread_file
Writewrite_file
Globglob
Grepgrep
LSls

Other differences: Gemini CLI has no PermissionRequest or SubagentStart event, exit code 1 is a warning rather than an error, and BeforeModel, AfterModel and BeforeToolSelection have no Claude Code equivalent. CLAUDE_PROJECT_DIR is still set in the hook environment, so scripts that read it keep working.

Hook input and output fields#

Every hook receives these fields on stdin, plus the event-specific fields below:

FieldTypeAlways sentNotes
session_idstringyes—
transcript_pathstringyes—
cwdstringyes—
hook_event_namestringyes—
timestampstringyes—

BeforeTool#

FieldTypeAlways sentNotes
tool_namestringyes—
tool_inputRecord<string, unknown>yes—
mcp_contextobjectnoOnly present for MCP tools
mcp_context.server_namestringyes—
mcp_context.tool_namestringyesOriginal tool name from the MCP server
mcp_context.commandstringnoFor stdio transport
mcp_context.argsstring[]noFor stdio transport
mcp_context.cwdstringnoFor stdio transport
mcp_context.urlstringnoFor SSE/HTTP transport
mcp_context.tcpstringnoFor WebSocket transport
original_request_namestringno—
FieldTypeNotes
hookSpecificOutput.tool_inputRecord<string, unknown>—

AfterTool#

FieldTypeAlways sentNotes
tool_namestringyes—
tool_inputRecord<string, unknown>yes—
tool_responseRecord<string, unknown>yes—
mcp_contextobjectnoOnly present for MCP tools
mcp_context.server_namestringyes—
mcp_context.tool_namestringyesOriginal tool name from the MCP server
mcp_context.commandstringnoFor stdio transport
mcp_context.argsstring[]noFor stdio transport
mcp_context.cwdstringnoFor stdio transport
mcp_context.urlstringnoFor SSE/HTTP transport
mcp_context.tcpstringnoFor WebSocket transport
original_request_namestringno—
FieldTypeNotes
hookSpecificOutput.additionalContextstring—
hookSpecificOutput.tailToolCallRequestobjectOptional request to execute another tool immediately after this one. The result of this tail call will replace the original tool's response.
hookSpecificOutput.tailToolCallRequest.namestring—
hookSpecificOutput.tailToolCallRequest.argsRecord<string, unknown>—

BeforeAgent#

FieldTypeAlways sentNotes
promptstringyes—
FieldTypeNotes
hookSpecificOutput.additionalContextstring—

AfterAgent#

FieldTypeAlways sentNotes
promptstringyes—
prompt_responsestringyes—
stop_hook_activebooleanyes—
FieldTypeNotes
hookSpecificOutput.clearContextboolean—

Notification#

FieldTypeAlways sentNotes
notification_typeToolPermissionyes—
messagestringyes—
detailsRecord<string, unknown>yes—

SessionStart#

FieldTypeAlways sentNotes
sourcestartup | resume | clearyes—
FieldTypeNotes
hookSpecificOutput.additionalContextstring—

SessionEnd#

FieldTypeAlways sentNotes
reasonexit | clear | logout | prompt_input_exit | otheryes—

PreCompress#

FieldTypeAlways sentNotes
triggermanual | autoyes—

BeforeModel#

FieldTypeAlways sentNotes
llm_requestLLMRequestyes—
FieldTypeNotes
hookSpecificOutput.llm_requestPartial<LLMRequest>—
hookSpecificOutput.llm_responseLLMResponse—

AfterModel#

FieldTypeAlways sentNotes
llm_requestLLMRequestyes—
llm_responseLLMResponseyes—
FieldTypeNotes
hookSpecificOutput.llm_responsePartial<LLMResponse>—

BeforeToolSelection#

FieldTypeAlways sentNotes
llm_requestLLMRequestyes—
FieldTypeNotes
hookSpecificOutput.toolConfigobject—
hookSpecificOutput.toolConfig.mode'AUTO' | 'ANY' | 'NONE'—
hookSpecificOutput.toolConfig.allowedFunctionNamesstring[]—

Frequently asked questions#

Does Gemini CLI support hooks?#

Yes. Gemini CLI has 11 hook events, including BeforeTool, AfterTool, BeforeAgent, AfterAgent, SessionStart, SessionEnd, PreCompress and Notification, plus three model-level events. Hooks are shell commands configured in the hooks block of settings.json or in an extension's hooks/hooks.json, and the system is on by default through hooksConfig.enabled.

Why are my Gemini CLI hooks not working?#

The usual causes: the folder is not trusted, so settings hooks are skipped; hooksConfig.enabled is false or the hook's name is in hooksConfig.disabled; the matcher is written as /pattern/, which the regex treats as literal slashes; the timeout was set in seconds (30 means 30 ms); or stdout mixes log lines with JSON, so the whole output is read as plain text.

Is the Gemini CLI hook timeout in seconds or milliseconds?#

Milliseconds. The default is 60000 (60 seconds). When it expires Gemini CLI sends SIGTERM, then SIGKILL five seconds later, and reports the hook as failed. Hooks converted with gemini hooks migrate --from-claude keep Claude Code's value unchanged, so a Claude timeout of 30 seconds becomes 30 milliseconds unless you multiply it by 1000.

How do I send a notification when Gemini CLI needs input?#

Add a Notification hook. It fires with notification_type set to ToolPermission when a tool call is waiting for your confirmation, along with a message and a details object describing the call. The matcher is ignored for this event and the hook cannot change the outcome, so it suits desktop alerts, sounds or chat pings.

Can a Gemini CLI hook block a tool call?#

Yes, from BeforeTool. Print {"decision": "deny", "reason": "..."} and the call is skipped with Tool execution blocked: <reason>. Plain text with exit code 2 or higher also blocks, using the text as the reason; plain text with exit code 1 only produces a warning. "decision": "ask" forces a confirmation instead of blocking outright.

How do Gemini CLI hooks compare to Claude Code hooks?#

The configuration shape and stdin/stdout JSON contract match, but the names differ: PreToolUse is BeforeTool, PostToolUse is AfterTool, UserPromptSubmit is BeforeAgent, Stop is AfterAgent, PreCompact is PreCompress. Matchers use Gemini tool names such as run_shell_command and replace, and timeouts are milliseconds. gemini hooks migrate --from-claude converts an existing Claude Code configuration.

Do project hooks run without asking?#

Yes, in a trusted folder. New hooks in a project's .gemini/settings.json trigger a one-time warning that lists them, are then recorded in ~/.gemini/trusted_hooks.json, and run without an approval prompt. In an untrusted folder no hooks from settings.json run at all, so trusting a folder also means trusting its hooks.

Sources

Release-by-release changes: Gemini CLI version tracker. All Gemini CLI pages: Gemini CLI reference index.