Development
Hook
Hooks
What clawd-on-desk runs automatically, and when. A hook is a command Claude Code fires at a fixed moment, without you asking for it.
Where it lives
- hooks/antigravity-context-usage.jsGitHub
Read the script
"use strict"; const { normalizeQuotaGroup, anchorRelativeResetAt } = require("./quota-bucket"); // Antigravity's statusline payload (unlike Claude Code's transcript) already // reports the model's real context window size and fill level directly, so // there is no model-name -> limit table to maintain here. // Field names come from the community-documented statusline JSON contract: // https://github.com/weby-homelab/antigravity-cli-statusline function normalizeNonNegativeNumber(value) { const n = Number(value); return Number.isFinite(n) && n >= 0 ? n : null; } function resolveAntigravityContextUsage(payload) { const ctx = payload && typeof payload.context_window === "object" ? payload.context_window : null; if (!ctx) return null; const limit = normalizeNonNegativeNumber(ctx.context_window_size); const inputTokens = normalizeNonNegativeNumber(ctx.total_input_tokens); const outputTokens = normalizeNonNegativeNumber(ctx.total_output_tokens); const usedPercentage = normalizeNonNegativeNumber(ctx.used_percentage); let used = null; if (inputTokens !== null || outputTokens !== null) { used = (inputTokens || 0) + (outputTokens || 0); } else if (usedPercentage !== null && limit !== null) { used = Math.round((usedPercentage / 100) * limit); } if (used === null) return null; const out = { used, source: "antigravity" }; if (limit !== null && limit > 0) { out.limit = limit; out.percent = usedPercentage !== null ? Math.max(0, Math.min(100, Math.round(usedPercentage))) : Math.max(0, Math.min(100, Math.round((used / limit) * 100))); } return out; } function resolveAntigravityModelLabel(payload) { const model = payload && typeof payload.model === "object" ? payload.model : null; if (!model) return null; const displayName = typeof model.display_name === "string" && model.display_name.trim(); if (displayName) return displayName.trim(); const id = typeof model.id === "string" && model.id.trim(); return id ? id.trim() : null; } // Account-wide rate-limit quota (the same data agy's own `/usage` command // shows), not per-conversation context usage. agy reports `remaining_fraction` // (how much is left); we invert it to usedPercent at the parsing boundary so // every quota source in the app (agy, Claude Code) shares one "how much is // used" convention - see hooks/quota-bucket.js. const ANTIGRAVITY_QUOTA_FIELDS = ["geminiFiveHour", "geminiWeekly", "thirdPartyFiveHour", "thirdPartyWeekly"]; const QUOTA_BUCKET_KEYS = { "gemini-5h": "geminiFiveHour", "gemini-weekly": "geminiWeekly", "3p-5h": "thirdPartyFiveHour", "3p-weekly": "thirdPartyWeekly", }; function invertAntigravityQuotaPayload(quota) { const out = {}; const nowMs = Date.now(); for (const [key, field] of Object.entries(QUOTA_BUCKET_KEYS)) { const bucket = quota[key]; if (!bucket || typeof bucket !== "object") continue; const remaining = Number(bucket.remaining_fraction); if (!Number.isFinite(remaining)) continue; const entry = { usedPercent: (1 - Math.max(0, Math.min(1, remaining))) * 100 }; // agy reports a relative countdown (reset_in_seconds), not an absolute // instant - anchor it to receive time, minute-quantized against the // broadcast-storm jitter (see quota-bucket.js anchorRelativeResetAt). const resetAt = anchorRelativeResetAt(bucket.reset_in_seconds, nowMs); if (resetAt !== null) entry.resetAt = resetAt; out[field] = entry; } return out; } function resolveAntigravityQuota(payload) { const quota = payload && typeof payload.quota === "object" ? payload.quota : null; if (!quota) return null; return normalizeQuotaGroup(invertAntigravityQuotaPayload(quota), ANTIGRAVITY_QUOTA_FIELDS); } module.exports = { resolveAntigravityContextUsage, resolveAntigravityModelLabel, resolveAntigravityQuota, ANTIGRAVITY_QUOTA_FIELDS, }; - hooks/antigravity-hook.jsGitHub
Read the script
#!/usr/bin/env node // Clawd - Antigravity CLI hook adapter // Registered in Antigravity's global hooks file by hooks/antigravity-install.js const fs = require("fs"); const os = require("os"); const path = require("path"); const { postPermissionToRunningServer, postStateToRunningServer, readHostPrefix, applyWslSourceFields } = require("./server-config"); const { createPidResolver, readStdinJson, getPlatformConfig, applyOrcaPaneKey } = require("./shared-process"); const { stdoutForAntigravityEvent } = require("./antigravity-stdout"); const ANTIGRAVITY_PERMISSION_TIMEOUT_MS = 590000; const TOOL_INPUT_STRING_MAX = 2000; const TOOL_INPUT_ARRAY_MAX = 32; const TOOL_INPUT_OBJECT_KEYS_MAX = 64; const TOOL_INPUT_DEPTH_MAX = 6; const DEBUG_STRING_MAX = 2000; const DEBUG_TOOL_INPUT_STRING_MAX = 240; const DEBUG_OBJECT_KEYS_MAX = 32; const DEBUG_ARRAY_MAX = 16; const DEBUG_DEPTH_MAX = 4; const HOOK_MAP = { PreInvocation: { state: "thinking", event: "UserPromptSubmit" }, PreToolUse: { state: "working", event: "PreToolUse" }, PostToolUse: { state: "working", event: "PostToolUse" }, PostInvocation: { state: "idle", event: "AfterAgent" }, Stop: { state: "attention", event: "Stop" }, }; const config = getPlatformConfig(); function isAntigravityAgentCommandLine(cmd) { if (typeof cmd !== "string") return false; const normalized = cmd.toLowerCase().replace(/\\/g, "/"); return /(^|[\s"'/])agy(\.exe)?($|[\s"'/])/.test(normalized) || normalized.includes("/agy/bin/agy.exe") || normalized.includes("/antigravity-cli/"); } const resolve = createPidResolver({ agentNames: { win: new Set(["agy.exe"]), mac: new Set(["agy"]), linux: new Set(["agy"]) }, agentCmdlineCheck: isAntigravityAgentCommandLine, platformConfig: config, }); function getAntigravityPermissionTimeoutMs(env = process.env) { const raw = Number(env.CLAWD_ANTIGRAVITY_PERMISSION_TIMEOUT_MS); if (Number.isFinite(raw) && raw > 0) return Math.min(raw, ANTIGRAVITY_PERMISSION_TIMEOUT_MS); return ANTIGRAVITY_PERMISSION_TIMEOUT_MS; } function isTruthyDebugValue(value) { return value === "1" || value === "true" || value === "yes" || value === "on"; } function isAntigravityHookDebugEnabled(env = process.env) { return isTruthyDebugValue(String(env.CLAWD_ANTIGRAVITY_HOOK_DEBUG || "").toLowerCase()); } function getAntigravityHookDebugLogPath(env = process.env) { if (typeof env.CLAWD_ANTIGRAVITY_HOOK_DEBUG_FILE === "string" && env.CLAWD_ANTIGRAVITY_HOOK_DEBUG_FILE.trim()) { return env.CLAWD_ANTIGRAVITY_HOOK_DEBUG_FILE.trim(); } return path.join(os.homedir(), ".gemini", "antigravity-cli", "clawd-hook-debug.log"); } function truncateDebugString(value, max = DEBUG_STRING_MAX) { if (typeof value !== "string") return value; if (value.length <= max) return value; return `${value.slice(0, Math.max(0, max - 3))}...`; } function normalizeDebugValue(value, depth = 0) { if (depth > DEBUG_DEPTH_MAX) return "[truncated]"; if (Array.isArray(value)) { return value.slice(0, DEBUG_ARRAY_MAX).map((entry) => normalizeDebugValue(entry, depth + 1)); } if (value && typeof value === "object") { const out = {}; for (const key of Object.keys(value).sort().slice(0, DEBUG_OBJECT_KEYS_MAX)) { if (/token|secret|password|authorization|credential|api[_-]?key/i.test(key)) { out[key] = "[redacted]"; continue; } out[key] = normalizeDebugValue(value[key], depth + 1); } return out; } if (typeof value === "string") return truncateDebugString(value, DEBUG_TOOL_INPUT_STRING_MAX); if (value === null || typeof value === "number" || typeof value === "boolean") return value; return value === undefined ? undefined : String(value); } function writeAntigravityHookDebug(env, event, fields = {}) { if (!isAntigravityHookDebugEnabled(env)) return false; let line; try { line = JSON.stringify({ ts: new Date().toISOString(), event, ...fields, }); } catch { line = JSON.stringify({ ts: new Date().toISOString(), event: "debug-serialize-failed", originalEvent: event, }); } if (isTruthyDebugValue(String(env.CLAWD_ANTIGRAVITY_HOOK_DEBUG_STDERR || "").toLowerCase())) { try { process.stderr.write(`[clawd-antigravity] ${line}\n`); } catch {} } try { const debugPath = getAntigravityHookDebugLogPath(env); fs.mkdirSync(path.dirname(debugPath), { recursive: true }); fs.appendFileSync(debugPath, `${line}${os.EOL}`, "utf8"); return true; } catch { return false; } } function buildAntigravityNoDecisionOutput(reason) { const body = { decision: "ask" }; if (typeof reason === "string" && reason.trim()) body.reason = reason.trim(); return JSON.stringify(body); } function stdoutForEvent(hookName) { return stdoutForAntigravityEvent(hookName); } function resolveHookName(payload, argvEvent) { return (payload && payload.hookEventName) || (payload && payload.hook_event_name) || argvEvent || ""; } function shouldResolvePid(hookName, env = process.env) { return !!HOOK_MAP[hookName] && !env.CLAWD_REMOTE; } function normalizeSessionId(value, payload) { const fallback = payload && typeof payload.transcriptPath === "string" && payload.transcriptPath ? path.basename(path.dirname(payload.transcriptPath)) || "default" : "default"; const raw = value != null && value !== "" ? String(value) : fallback; return raw.startsWith("antigravity:") ? raw : `antigravity:${raw}`; } function resolveCwd(payload) { const toolArgs = payload && payload.toolCall && payload.toolCall.args; if (toolArgs && typeof toolArgs.Cwd === "string" && toolArgs.Cwd) return toolArgs.Cwd; if (payload && Array.isArray(payload.workspacePaths)) { const first = payload.workspacePaths.find((entry) => typeof entry === "string" && entry); if (first) return first; } return ""; } // #634: cross-process pid cache context for the shared resolver. Antigravity // has no session-start hook (earliest event is Pr - hooks/antigravity-install.jsGitHub
Read the script
#!/usr/bin/env node // Merge Clawd Antigravity hooks into ~/.gemini/config/hooks.json. const fs = require("fs"); const path = require("path"); const os = require("os"); const { resolveNodeBin } = require("./server-config"); const { stdoutForAntigravityEvent } = require("./antigravity-stdout"); const { readJsonFile, writeJsonAtomic, writeJsonAtomicWithBackup, asarUnpackedPath, buildPortableStatuslineCommand, decodeWindowsEncodedCommand, extractFirstQuotedToken, windowsPowerShellBin, } = require("./json-utils"); const HOOK_GROUP_ID = "clawd"; const MARKER = "antigravity-hook.js"; const DEFAULT_PARENT_DIR = path.join(os.homedir(), ".gemini", "config"); const DEFAULT_CONFIG_PATH = path.join(DEFAULT_PARENT_DIR, "hooks.json"); const STATUSLINE_MARKER = "antigravity-statusline.js"; const DEFAULT_STATUSLINE_SETTINGS_DIR = path.join(os.homedir(), ".gemini", "antigravity-cli"); const DEFAULT_STATUSLINE_SETTINGS_PATH = path.join(DEFAULT_STATUSLINE_SETTINGS_DIR, "settings.json"); // PreToolUse intentionally NOT registered. Antigravity 1.0.1 LLMs proactively // call the built-in `ask_permission` tool before sensitive actions, which then // triggers agy's native 5-option menu — there's no way for a hook to suppress // that menu. Layering a Clawd bubble on top of (or in front of) the native // menu yields 8-10 confirmations for a single user task. // Antigravity stays a state-only integration; agy native menu owns permission. const ANTIGRAVITY_HOOK_EVENTS = [ "PreInvocation", "PostToolUse", "PostInvocation", "Stop", ]; const DEFAULT_HOOK_TIMEOUT_SECONDS = 10; // #568 budget: stdin timeout + child timeout must stay below // DEFAULT_HOOK_TIMEOUT_SECONDS with real headroom, or the outer hooks.json // timeout kills the wrapper before the fallback line is printed. Measured // worst case (never-closed stdin + hung child) at 2+7 was 9.5-9.7s on a warm // machine — a PowerShell cold start under AV scanning would blow past 10s — // so the child watchdog stays at 6s to keep ~2s of startup headroom. const FAIL_OPEN_CHILD_TIMEOUT_SECONDS = 6; const FAIL_OPEN_STDIN_TIMEOUT_SECONDS = 2; function fallbackStdoutForEvent(event) { return stdoutForAntigravityEvent(event); } function quoteShellSingleArg(value) { return `'${String(value).replace(/'/g, "'\\''")}'`; } function quotePowerShellSingleArg(value) { return `'${String(value).replace(/'/g, "''")}'`; } function normalizeFailOpenTimeoutSeconds(options = {}) { const raw = Number(options.failOpenTimeoutSeconds); if (Number.isFinite(raw) && raw > 0) return Math.max(1, Math.floor(raw)); return FAIL_OPEN_CHILD_TIMEOUT_SECONDS; } function normalizeStdinTimeoutSeconds(options = {}) { const raw = Number(options.stdinTimeoutSeconds); if (Number.isFinite(raw) && raw > 0) return Math.max(1, Math.floor(raw)); return FAIL_OPEN_STDIN_TIMEOUT_SECONDS; } function quoteWindowsProcessArg(value) { const text = String(value); if (text && !/[\s"]/u.test(text)) return text; let out = '"'; let backslashes = 0; for (const ch of text) { if (ch === "\\") { backslashes++; continue; } if (ch === '"') { out += "\\".repeat((backslashes * 2) + 1); out += '"'; backslashes = 0; continue; } out += "\\".repeat(backslashes); backslashes = 0; out += ch; } out += "\\".repeat(backslashes * 2); out += '"'; return out; } function withFailOpenShellFallback(command, event, nodeBin, options = {}) { const fallback = quoteShellSingleArg(fallbackStdoutForEvent(event)); const timeoutSeconds = normalizeFailOpenTimeoutSeconds(options); const stdinTimeoutSeconds = normalizeStdinTimeoutSeconds(options); const validatorScript = [ "let s='';", "process.stdin.setEncoding('utf8');", "process.stdin.on('data',c=>s+=c);", "process.stdin.on('end',()=>{", "try{const v=JSON.parse(s);if(!v||typeof v!=='object'||Array.isArray(v))process.exit(1);}", "catch{process.exit(1);}", "});", ].join(""); const validatorCommand = [ nodeBin, "-e", validatorScript, ].map(quoteShellSingleArg).join(" "); return [ "tmp_dir=${TMPDIR:-/tmp}", "in_file=$(mktemp \"$tmp_dir/clawd-agy-in.XXXXXX\" 2>/dev/null || printf '%s/clawd-agy-in-%s' \"$tmp_dir\" \"$$\")", "out_file=$(mktemp \"$tmp_dir/clawd-agy-out.XXXXXX\" 2>/dev/null || printf '%s/clawd-agy-out-%s' \"$tmp_dir\" \"$$\")", "pid=", "watchdog=", // Do not trap TERM: macOS bash 3.2 may print run_pending_traps warnings when the watchdog is killed. "cleanup(){ trap - EXIT HUP INT TERM; [ -n \"$watchdog\" ] && kill \"$watchdog\" 2>/dev/null; [ -n \"$pid\" ] && kill \"$pid\" 2>/dev/null; rm -f \"$in_file\" \"$out_file\"; }", "trap cleanup EXIT HUP INT", // #568: IDE/App hook runners may never close our stdin, so the stdin read // needs its own watchdog. Background lists get /dev/null as stdin in // non-interactive shells; the 3<&0 group redirection hands the real stdin // to the background cat (and fails soft if fd 0 is somehow absent). "{ cat <&3 > \"$in_file\" 2>/dev/null & pid=$!; } 3<&0", // Watchdog subshells redirect stdout/stderr: a killed watchdog orphans its // sleep, and an orphan holding our stdout would stall a hook runner that // waits for pipe EOF instead of process exit. `( sleep ${stdinTimeoutSeconds}; kill "$pid" 2>/dev/null ) > /dev/null 2>&1 & watchdog=$!`, "wait \"$pid\" 2>/dev/null", "[ -n \"$watchdog\" ] && kill \"$watchdog\" 2>/dev/null", "[ -n \"$watchdog\" ] && wait \"$watchdog\" 2>/dev/null", "pid=", "watchdog=", `${command} < "$in_file" > "$out_file" 2>/dev/null & pid=$!`, `( sleep ${timeoutSeconds}; kill "$pid" 2>/dev/null ) > /dev/null 2>&1 & watchdog=$!`, "wait \"$pid\" 2>/dev/null", "status=$?", "[ -n \"$watchdog\" ] && kill \"$watchdog\" 2>/dev/null", "[ -n \"$watchdog\" ] && wait \"$watchdog\" 2>/dev/null", "pid=", "watchdog=", "out=$(cat \"$out_file\" 2>/dev/null - hooks/antigravity-statusline.jsGitHub
Read the script
#!/usr/bin/env node // Clawd - Antigravity CLI statusline adapter. // Registered as `statusLine.command` in ~/.gemini/antigravity-cli/settings.json // by hooks/antigravity-install.js. Antigravity pipes a JSON telemetry payload // (agent state, context window usage, model, cwd, etc.) to stdin on every // statusline refresh and renders whatever we write to stdout as the terminal // status line. // // Unlike the PreInvocation/PostToolUse/PostInvocation/Stop hooks in // antigravity-hook.js (which only drive Clawd's own session state), this // script also owns rendering visible terminal text, so it must always print // *something* fast and never throw - a stuck or crashed statusline script // would blank out the user's real Antigravity CLI status line. const { applyWslSourceFields, postStateToRunningServer, readHostPrefix, } = require("./server-config"); const { readStdinJson } = require("./shared-process"); const { resolveAntigravityContextUsage, resolveAntigravityModelLabel, resolveAntigravityQuota, } = require("./antigravity-context-usage"); const STATE_POST_TIMEOUT_MS = 150; function normalizeSessionId(conversationId) { const raw = conversationId != null && conversationId !== "" ? String(conversationId) : "default"; return raw.startsWith("antigravity:") ? raw : `antigravity:${raw}`; } function buildStatusLineText(payload, contextUsage, modelLabel) { const parts = []; if (modelLabel) parts.push(modelLabel); if (contextUsage && Number.isFinite(contextUsage.percent)) parts.push(`${contextUsage.percent}% ctx`); const state = payload && typeof payload.agent_state === "string" ? payload.agent_state : null; if (state) parts.push(state); return parts.length ? parts.join(" · ") : ""; } function buildStateBody(payload, contextUsage, quota, options = {}) { const conversationId = payload && payload.conversation_id; if (!conversationId) return null; // metadata_only routes this around the updateSession lifecycle machine // entirely (src/server-route-state.js + state.js updateSessionMetadata): // context/quota are annotated onto an existing session and dropped // otherwise - never creating a session, touching recentEvents, or bumping // updatedAt. That also sidesteps all event-keyed bookkeeping concerns // (tool boundaries, post-Stop drop guards). state/preserve_state stay as // a defensive fallback shape only. const body = { state: "idle", preserve_state: true, metadata_only: true, session_id: normalizeSessionId(conversationId), agent_id: "antigravity-cli", }; const cwd = payload && typeof payload.cwd === "string" ? payload.cwd : ""; if (cwd) body.cwd = cwd; if (contextUsage) body.context_usage = contextUsage; if (quota) body.antigravity_quota = quota; if (options.remote) { body.host = options.host || readHostPrefix(); } return (options.applyWslSourceFields || applyWslSourceFields)(body, { remote: !!options.remote, }); } function postStateBody(body, deps, env) { if (!body) return Promise.resolve(false); const postState = deps.postState || postStateToRunningServer; return new Promise((resolve) => { postState(JSON.stringify(body), { timeoutMs: STATE_POST_TIMEOUT_MS, env }, (posted) => resolve(!!posted)); }); } async function main(deps = {}) { const env = deps.env || process.env; let payload = null; try { payload = deps.payload !== undefined ? deps.payload : await (deps.readStdinJson || readStdinJson)(); } catch { payload = null; } let contextUsage = null; let quota = null; let modelLabel = null; let text = ""; try { contextUsage = resolveAntigravityContextUsage(payload); quota = resolveAntigravityQuota(payload); modelLabel = resolveAntigravityModelLabel(payload); text = buildStatusLineText(payload, contextUsage, modelLabel); } catch { // fall through with whatever defaults were already assigned } try { const remote = !!env.CLAWD_REMOTE; const body = buildStateBody(payload, contextUsage, quota, { remote, host: remote && deps.readHostPrefix ? deps.readHostPrefix() : undefined, applyWslSourceFields: deps.applyWslSourceFields, }); await postStateBody(body, deps, env); } catch { // Never let a failed/slow POST take down the visible status line. } process.stdout.write(`${text}\n`); } if (require.main === module) { main().catch(() => { process.stdout.write("\n"); }).finally(() => { process.exit(0); }); } module.exports = { __test: { normalizeSessionId, buildStatusLineText, buildStateBody, postStateBody, main, }, }; - hooks/antigravity-stdout.jsGitHub
Read the script
"use strict"; function stdoutForAntigravityEvent(hookName) { if (hookName === "PreToolUse") return JSON.stringify({ decision: "ask" }); if (hookName === "Stop") return JSON.stringify({ decision: "allow" }); return "{}"; } module.exports = { stdoutForAntigravityEvent, }; - hooks/appimage-hook-materializer.jsGitHub
Read the script
"use strict"; // Shared, agent-agnostic AppImage hook materializer. // // AppImage mounts live under a transient FUSE path (/tmp/.mount_*). Persisting // an absolute hook command that points into that mount dies as soon as the // AppImage exits. This module copies the entry points and their static // relative-require closure into a content-addressed, persistent generation // under the user's home and returns the stable target paths callers must // register. // // Leaf module by contract: it may only depend on Node builtins and the // APPIMAGE marker constant in ./server-config. Importing hooks/install.js or // hooks/codex-install-utils.js from here would create a cycle // (install.js -> materializer -> install.js) and hand one side a half-built // module.exports. const fs = require("fs"); const path = require("path"); const os = require("os"); const crypto = require("crypto"); const { APPIMAGE_HOOK_MARKER_FILE } = require("./server-config"); // Literal CJS requires used by our hooks, not a general JavaScript parser. // Shared by install.js's dependency preflight and the closure collector below // so both recognize the same whitespace / quote / extensionless grammar. const HOOK_RELATIVE_REQUIRE_RE = /\brequire\s*\(\s*(["'])(\.\.?\/[^"'\r\n]+)\1\s*\)/g; class AppImageHookMaterializerError extends Error { constructor(code, message, details) { super(message); this.name = "AppImageHookMaterializerError"; this.code = code; this.details = details || null; } } function scanRelativeRequires(source) { const specs = []; for (const match of String(source || "").matchAll(HOOK_RELATIVE_REQUIRE_RE)) { specs.push(match[2]); } return specs; } function normalizeEntryPaths(entryPaths, options = {}) { const list = []; if (Array.isArray(entryPaths)) list.push(...entryPaths); else if (typeof entryPaths === "string" && entryPaths) list.push(entryPaths); if (Array.isArray(options.extraEntryPaths)) list.push(...options.extraEntryPaths); const seen = new Set(); const normalized = []; for (const value of list) { if (typeof value !== "string" || !value) continue; const resolved = path.resolve(value); if (seen.has(resolved)) continue; seen.add(resolved); normalized.push(resolved); } return normalized; } function isTextuallyInside(rootDir, target) { const relative = path.relative(rootDir, target); return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative)); } // Exact compatibility grammar for commands written by released AppImage // builds before hook materialization existed. Basename alone is never enough: // the path must be rooted in the AppImage FUSE mount and end at the packaged // hooks entry. This is intentionally lexical because the old mount is normally // gone by the time an upgrade repairs the persisted command. function isLegacyAppImageHookPath(value, filename) { const normalized = String(value || "").replace(/\\/g, "/"); const escaped = String(filename || "").replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); if (!escaped || /[\/\\]/.test(String(filename || ""))) return false; return new RegExp( // AppImage uses the first six basename characters plus six random // alphanumerics. Released Clawd artifacts used `Clawd-on-Desk-*` (and the // older electron-builder default `Clawd on Desk-*`), so their exact mount // prefixes are `Clawd-` and `Clawd `. A broad `Clawd*` prefix would still // claim an unrelated `ClawdSomething.AppImage` mount (`ClawdSXXXXXX`). `^/tmp/\\.mount_Clawd(?:-| )[A-Za-z0-9]{6}(?:/[^/]+)*/resources/app\\.asar\\.unpacked/hooks/${escaped}$` ).test(normalized); } // A sentinel-bearing command may survive a release change while its old // content-addressed generation remains valid. Accept only the exact managed // layout (one 20-hex generation directory plus the expected entry filename), // never an arbitrary same-basename path. function isManagedAppImageHookTarget(value, filename, options = {}) { const root = options.materializedRoot || path.join(options.homeDir || os.homedir(), ".clawd", "appimage-hooks"); const normalizedRoot = String(path.resolve(root)).replace(/\\/g, "/").replace(/\/+$/, ""); const normalizedValue = String(value || "").replace(/\\/g, "/"); const relative = normalizedValue.startsWith(`${normalizedRoot}/`) ? normalizedValue.slice(normalizedRoot.length + 1) : ""; if (!relative) return false; const parts = relative.split("/"); return parts.length === 2 && /^[a-f0-9]{20}$/.test(parts[0]) && parts[1] === filename; } // The hooks root is an explicit boundary, never "the common ancestor of every // entry" — the latter would silently widen to the repo root when a caller adds // an entry under agents/ or src/, accepting files outside hooks/. Callers that // know the directory (the Claude resolver, install.js) pass `rootDir`; the // compatibility wrapper safely defaults to the primary entry's own directory, // which for a same-directory multi-entry bundle is order-independent. function resolveHookRootDir(entryPaths, options = {}) { if (typeof options.rootDir === "string" && options.rootDir.trim()) { return path.resolve(options.rootDir.trim()); } if (!entryPaths.length) { throw new AppImageHookMaterializerError( "NO_ENTRIES", "AppImage hook materialization requires at least one entry path" ); } return path.dirname(entryPaths[0]); } // Returns the realpath, or null only when the path genuinely does not exist. // Any other realpath failure (EACCES/EIO/ELOOP/...) must fail closed: treating // it as "unverifiable, proceed" would let a symlink escape or an unreadable // ancestor be trusted. function realpathOrNull(target, fsApi, realpathSync) { const fn = typeof realpathSync === "function" ? realpathSync : (typeof fsApi.realpathSync === "function" ? fsApi.realpathSync.bind(fsApi) : null); if (!fn) return null; // no realpath capability injected (e.g. a minimal test fs) try { - hooks/auto-start.jsGitHub
- hooks/claude-rate-limits.jsGitHub
- hooks/claude-session-id.jsGitHub
- hooks/claude-statusline-local-chain.jsGitHub
- hooks/claude-statusline.jsGitHub
- hooks/claude-stop-disposition.jsGitHub
- hooks/clawd-hook.jsGitHub
- hooks/cleanup-integrations.jsGitHub
- hooks/codebuddy-hook.jsGitHub
- hooks/codebuddy-install.jsGitHub
- hooks/codewhale-hook.jsGitHub
- hooks/codewhale-install.jsGitHub
- hooks/codex-assistant-output.jsGitHub
- hooks/codex-debug-hook.jsGitHub
- hooks/codex-debug-install.jsGitHub
- hooks/codex-hook.jsGitHub
- hooks/codex-install-utils.jsGitHub
- hooks/codex-install.jsGitHub
- hooks/codex-originator.jsGitHub
- hooks/codex-rate-limits.jsGitHub
- hooks/codex-remote-monitor.jsGitHub
- hooks/codex-session-index.jsGitHub
- hooks/codex-subagent-fields.jsGitHub
- hooks/codex-user-input.jsGitHub
- hooks/context-usage.jsGitHub
- hooks/copilot-hook.jsGitHub
- hooks/copilot-install.jsGitHub
- hooks/cursor-hook.jsGitHub
- hooks/cursor-install.jsGitHub
- hooks/cursor-session-title.jsGitHub
- hooks/dsh-install.jsGitHub
- hooks/gemini-hook.jsGitHub
- hooks/gemini-install.jsGitHub
- hooks/grok-hook.jsGitHub
- hooks/grok-install.jsGitHub
- hooks/hermes-install.jsGitHub
- hooks/hook-dependency-preflight.jsGitHub
- hooks/install.jsGitHub
- hooks/json-utils.jsGitHub
- hooks/kimi-hook.jsGitHub
- hooks/kimi-install.jsGitHub
- hooks/kimi-process-names.jsGitHub
- hooks/kiro-hook.jsGitHub
- hooks/kiro-install.jsGitHub
- hooks/mimocode-install.jsGitHub
- hooks/omp-extension-core.jsGitHub
- hooks/omp-extension.tsGitHub
- hooks/omp-install.jsGitHub
- hooks/openclaw-install.jsGitHub
- hooks/opencode-family-entry-ownership.jsGitHub
- hooks/opencode-family-install.jsGitHub
- hooks/opencode-family-jsonc.jsGitHub
- hooks/opencode-family-managed-generation.jsGitHub
- hooks/opencode-install.jsGitHub
- hooks/pi-extension-core.jsGitHub
- hooks/pi-extension.tsGitHub
- hooks/pi-install.jsGitHub
- hooks/pid-cache.jsGitHub
- hooks/qoder-hook.jsGitHub
- hooks/qoder-install.jsGitHub
- hooks/qoderwork-hook.jsGitHub
- hooks/qoderwork-install.jsGitHub
- hooks/quota-bucket.jsGitHub
- hooks/qwen-code-hook.jsGitHub
- hooks/qwen-code-install.jsGitHub
- hooks/qwenwork-hook.jsGitHub
- hooks/qwenwork-install.jsGitHub
- hooks/reasonix-hook.jsGitHub
- hooks/reasonix-install.jsGitHub
- hooks/server-config.jsGitHub
- hooks/session-history.jsGitHub
- hooks/session-recovery-lease.jsGitHub
- hooks/shared-process.jsGitHub
- hooks/state-payload-size.jsGitHub
- hooks/traecode-hook.jsGitHub
- hooks/traecode-install.jsGitHub
- hooks/uninstall.jsGitHub
- hooks/workbuddy-hook.jsGitHub
- hooks/workbuddy-install.jsGitHub
- hooks/wsl-connectivity-probe.jsGitHub
- hooks/zcode-hook.jsGitHub
- hooks/zcode-install.jsGitHub
All 88 scripts are listed above. The source is inlined for 6 of them, starting with whatever hooks.json actually runs. See all of them in the repo.
Read the script before you install anything that runs on your machine. This is the one part of a plugin that acts without being asked.
Ships withclawd-on-desk
A pixel desktop pet that watches Claude Code, Codex, Cursor & other AI coding agents — so you don't have to.
Get the whole plugin
Stats
6,265
Stars
652
Forks
Active
Maintenance
JavaScript
Language
AGPL-3.0
License
14h ago
Last commit
6mo ago
Created
Repo: rullerzhou-afk/clawd-on-desk

