Hooks
RightNow Agent is coming soon for Windows, macOS and Linux. Installation commands and workflows in these guides are reference material until the public release is available.
Hooks run a command or send an HTTPS request when a session, turn, tool call, subagent, or compaction event fires.
Hook events
| Event | Fires when |
|---|---|
| SessionStart | Session starts |
| UserPromptSubmit | Prompt submitted |
| PreToolUse | Before a tool call; can deny or rewrite it |
| PostToolUse | Tool call succeeded |
| PostToolUseFailure | Tool call failed |
| PermissionDenied | Permission system denied a call |
| Stop | Main agent completes a turn; can block the stop |
| StopFailure | Turn ends with an API error |
| StopCancelled | Turn is interrupted or cancelled |
| Notification | User-attention event |
| SubagentStart | Subagent starts |
| SubagentStop | Subagent turn completes; can block the stop |
| PreCompact | Before compaction |
| PostCompact | After compaction |
| SessionEnd | Session ends |
SubagentEnd is accepted as an alias for SubagentStop. Every other event only observes.
Define hooks in JSON or TOML
| File | Scope |
|---|---|
| ~/.rightnow/ | User. |
| <repo>/ | Project, at the repository root. |
| ~/.rightnow/ | Config layers. All run; duplicates run once. |
| hooks/ | Plugin. |
Save this usable Windows guard as bin/safety-check.ps1. It reads one event from standard input and denies a recursive Remove-Item request; otherwise it exits successfully without output.
$hookPayload = [Console]::In.ReadToEnd() | ConvertFrom-Json
$command = [string]$hookPayload.toolInput.command
if ($hookPayload.toolName -eq "run_terminal_command" -and $command -match "(?i)\bRemove-Item\b.*-Recurse\b") {
@{ decision = "deny"; reason = "Recursive removal requires manual review" } | ConvertTo-Json -Compress
}The next two blocks are configuration templates for that script. Choose JSON or TOML; do not install both copies of the same hook.
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{ "type": "command", "command": "powershell -NoProfile -File bin/safety-check.ps1", "timeout": 10 }
]
}
]
}
}[[hooks.PreToolUse]]
matcher = "Bash"
hooks = [
{ type = "command", command = "powershell -NoProfile -File bin/safety-check.ps1", timeout = 10 },
]Handler fields: type (command or http), command, url, env, and timeout in seconds (default 5; 600 for Stop and SubagentStop). A direct executable path in a JSON hook file resolves relative to that file. The inline PowerShell command above starts in the workspace root, so its bin/safety-check.ps1 path is workspace-relative.
Matcher rules
- An empty or omitted matcher, or
*, matches every invocation in its group. A plain name or|list made only of letters, digits, and underscores matches only a value equal to one of its terms; any other pattern is an unanchored regular expression. - Tool events match the resolved tool name, notifications match their type, and subagent events match the subagent type. MCP calls match as
server__tool, sogithubmatches no MCP tool while^github__matches every tool of that server. StopandUserPromptSubmitignore matchers with a warning.- Claude-style matcher names such as Bash, Read, Edit, Write, Grep, and WebSearch map to RightNow tool names while retaining their original match.
Read the stdin envelope
Each event sends camelCase common fields plus its own. This PreToolUse example is complete; transcript, client, and subagent fields appear only when available.
{
"hookEventName": "pre_tool_use",
"sessionId": "session-id",
"cwd": "C:\\work\\project",
"workspaceRoot": "C:\\work\\project",
"timestamp": "2026-04-14T12:00:00Z",
"promptId": "prompt-id",
"permissionMode": "default",
"toolName": "run_terminal_command",
"toolUseId": "tool-use-id",
"toolInput": { "command": "rightnow doctor", "description": "Check terminal support" },
"toolInputTruncated": false
}Deny or rewrite a tool call
A PreToolUse hook allows, denies, or rewrites the call. Emit one JSON response on stdout for each invocation; no output allows it.
Deny
{ "decision": "deny", "reason": "Unsafe command detected" }Rewrite
{
"hookSpecificOutput": {
"updatedInput": { "command": "rightnow doctor", "description": "Check terminal support" }
}
}updatedInputmust be a JSON object. It replaces the whole input that plan mode, the permission prompt, and the tool see, sorun_terminal_commandneeds bothcommandanddescription.- The last valid rewrite wins, a deny discards every rewrite, and a rewrite that fails the tool schema is blocked as invalid input.
- Any other
decisionvalue is an error, and the hook fails open.
Gate completion with Stop
A Stop or SubagentStop hook decides whether the turn may end.
| Output | Effect |
|---|---|
{"decision": "block", "reason": "..."} | Keep working. The reason is fed back to the model in the same turn. |
{"hookSpecificOutput": {"additionalContext": "..."}} | Keep working, shown as feedback rather than an error. |
{"continue": false, "stopReason": "..."} | End the turn, overriding any block. |
No output, exit 0 | Allow the stop. |
Input adds stopHookActive (true during a continuation), lastAssistantMessage, and reason. Interrupts and API errors fire StopCancelled and StopFailure instead, and the session-end Stop cannot block. After eight continuations in one turn, the turn ends.
Exit-code behavior
- Exit code 0 allows the action when no blocking JSON decision is returned.
- Exit code 2 denies
PreToolUseor blocksStopandSubagentStop, using stderr as feedback when valid JSON is absent. - Other nonzero exits fail open when stdout has no valid blocking decision: the failure is recorded, but the action is not blocked.
Timeouts, crashes, malformed output, unset variables, and most nonzero exits are recorded and never block. Only a deny or block decision, or exit 2, does.
Hook environment
Every command hook receives RIGHTNOW_HOOK_EVENT, RIGHTNOW_HOOK_NAME, RIGHTNOW_SESSION_ID, RIGHTNOW_WORKSPACE_ROOT, and the compatibility alias CLAUDE_PROJECT_DIR. These names are reserved and handler env values cannot override them.
Handler command and url fields expand ${VAR} and $VAR when the hook runs. A shell command that references an unset variable without a default does not run.
HTTP hooks
Template: replace the URL with an endpoint you control before adding this handler to a hook group.
{ "type": "http", "url": "https://hooks.example.com/rightnow-event", "timeout": 15 }HTTP hooks POST the full event envelope as JSON. Only https:// URLs are accepted; private, link-local, and cloud-metadata addresses are refused (loopback is allowed), and redirects are not followed. A gate reads its decision from the response body.
Claude Code and Cursor compatibility
- Claude Code hooks load from
settings.jsonandsettings.local.jsonin~/.claude/and the project's.claude/([compat.claude] hooks, on by default) until/import-claudeimports or dismisses Claude settings. - Cursor hooks load from
~/.cursor/hooks.jsonand the project's.cursor/hooks.json([compat.cursor] hooks, on by default); camelCase event names map to RightNow events. - Input keys are camelCase, such as
stopHookActiveandtoolResult, andUserPromptSubmitis observe-only, so a ported prompt-blocking hook stops blocking. - RightNow Agent 1.0.5 runs project hook files without a folder-trust prompt. Review a repository's
.rightnow/hooks/,.claude/, and.cursor/files first; every hook runs with your privileges.
/hooks opens the Hooks tab. If a hook does not run, confirm its source and status in the Hooks tab, then inspect the failure entry in the session scrollback or logs and continue with troubleshooting.