Skip to content
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.

From plugin
clawd-on-desk
5.9k69 hooks

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/auto-start.jsGitHub
    Read the script
    #!/usr/bin/env node
    // Clawd Desktop Pet — Auto-Start Script
    // Registered as a SessionStart hook BEFORE clawd-hook.js.
    // Checks if the Electron app is running; if not, launches it detached.
    // Uses shared server discovery helpers and should exit quickly in normal cases.
    
    const { spawn } = require("child_process");
    const fs = require("fs");
    const path = require("path");
    const {
      APPIMAGE_HOOK_MARKER_FILE,
      discoverClawdPort,
    } = require("./server-config");
    const { buildElectronLaunchConfig } = require("./shared-process");
    
    const INITIAL_DISCOVER_TIMEOUT_MS = 300;
    const STARTUP_READY_TIMEOUT_MS = 6000;
    const STARTUP_DISCOVER_TIMEOUT_MS = 100;
    const STARTUP_POLL_INTERVAL_MS = 100;
    
    function waitForClawdPort(options, callback) {
      const discover = options.discoverClawdPort || discoverClawdPort;
      const setTimeoutFn = options.setTimeout || setTimeout;
      const nowFn = options.now || Date.now;
      const timeoutMs = Number.isFinite(options.timeoutMs) ? options.timeoutMs : STARTUP_READY_TIMEOUT_MS;
      const discoverTimeoutMs = Number.isFinite(options.discoverTimeoutMs)
        ? options.discoverTimeoutMs
        : STARTUP_DISCOVER_TIMEOUT_MS;
      const intervalMs = Number.isFinite(options.intervalMs) ? options.intervalMs : STARTUP_POLL_INTERVAL_MS;
      const deadline = nowFn() + Math.max(0, timeoutMs);
    
      function probe() {
        discover({ timeoutMs: discoverTimeoutMs }, (port) => {
          if (port || nowFn() >= deadline) {
            callback(port || null);
            return;
          }
          setTimeoutFn(probe, intervalMs);
        });
      }
    
      probe();
    }
    
    function decodeXmlText(value) {
      return String(value || "")
        .replace(/&lt;/g, "<")
        .replace(/&gt;/g, ">")
        .replace(/&quot;/g, "\"")
        .replace(/&apos;/g, "'")
        .replace(/&amp;/g, "&");
    }
    
    function resolveMacBundleExecutable(appBundle, options = {}) {
      const fsApi = options.fs || fs;
      let executableName = null;
      try {
        const plist = fsApi.readFileSync(
          path.posix.join(appBundle, "Contents", "Info.plist"),
          "utf8"
        );
        const match = plist.match(
          /<key>\s*CFBundleExecutable\s*<\/key>\s*<string>\s*([^<]+?)\s*<\/string>/i
        );
        const candidate = match ? decodeXmlText(match[1]).trim() : "";
        if (
          candidate
          && candidate !== "."
          && candidate !== ".."
          && !candidate.includes("/")
          && !candidate.includes("\\")
        ) {
          executableName = candidate;
        }
      } catch {}
      // electron-builder's productName is the stable executable name even when a
      // user renames or copies the outer .app bundle in Finder.
      if (!executableName) executableName = "Clawd on Desk";
      return path.posix.join(appBundle, "Contents", "MacOS", executableName);
    }
    
    function resolveAppImageExecutable(hooksDir, options = {}) {
      const fsApi = options.fs || fs;
      const candidates = [];
      try {
        candidates.push(
          fsApi.readFileSync(path.posix.join(hooksDir, APPIMAGE_HOOK_MARKER_FILE), "utf8")
        );
      } catch {}
      // APPIMAGE belongs to the running AppImage process, not arbitrary source
      // shells. Only trust the environment fallback while executing from the
      // packaged asar tree; materialized hooks use the adjacent marker above.
      if (hooksDir.includes("app.asar")) {
        const env = options.env || process.env;
        candidates.push(
          typeof options.appImagePath === "string" ? options.appImagePath : "",
          env && typeof env.APPIMAGE === "string" ? env.APPIMAGE : ""
        );
      }
      for (const value of candidates) {
        const candidate = String(value || "").trim();
        if (candidate && path.posix.isAbsolute(candidate)) return candidate;
      }
      return null;
    }
    
    function spawnDetached(spawnProcess, command, args, options, onError) {
      const child = spawnProcess(command, args, options);
      if (child && typeof child.once === "function") {
        child.once("error", (err) => {
          onError(err);
        });
      }
      if (child && typeof child.unref === "function") child.unref();
      return child;
    }
    
    function main(deps = {}) {
      const discover = deps.discoverClawdPort || discoverClawdPort;
      const launch = deps.launchApp || launchApp;
      const exit = deps.exit || ((code) => process.exit(code));
    
      discover({ timeoutMs: INITIAL_DISCOVER_TIMEOUT_MS }, (port) => {
        if (port) {
          exit(0);
          return;
        }
        launch();
        waitForClawdPort({
          discoverClawdPort: discover,
          setTimeout: deps.setTimeout,
          now: deps.now,
          timeoutMs: deps.startupReadyTimeoutMs,
          discoverTimeoutMs: deps.startupDiscoverTimeoutMs,
          intervalMs: deps.startupPollIntervalMs,
        }, () => exit(0));
      });
    }
    
    function launchApp(options = {}) {
      const hooksDir = options.hooksDir || __dirname;
      const platform = options.platform || process.platform;
      const spawnProcess = options.spawn || spawn;
      const onSpawnError = options.onSpawnError || ((err) => {
        process.stderr.write(`clawd auto-start: ${err && err.message ? err.message : err}\n`);
      });
      const isWin = platform === "win32";
      const isMac = platform === "darwin";
      const appImage = platform === "linux"
        ? resolveAppImageExecutable(hooksDir, options)
        : null;
      const isPackaged = hooksDir.includes("app.asar") || !!appImage;
    
      try {
        if (isPackaged) {
          if (isWin) {
            // __dirname: <install>/resources/app.asar.unpacked/hooks
            // exe:       <install>/Clawd on Desk.exe
            const installDir = path.resolve(hooksDir, "..", "..", "..");
            const exe = path.join(installDir, "Clawd on Desk.exe");
            spawnDetached(
              spawnProcess,
              exe,
              [],
              { detached: true, stdio: "ignore" },
              onSpawnError
            );
          } else if (isMac) {
            // __dirname: <name>.app/Contents/Resources/app.asar.unpacked/hooks
            // .app bundle: 4 levels up
            const appBundle = path.posix.resolve(hooksDir, "..", "..", "..", "..");
            const executable = resolveMacBundleExecutable(appBundle, {
              fs: options.fs,
            });
            // Launch the bundle executable directly. LaunchServices can create a
           
  • hooks/claude-rate-limits.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/gemini-hook.jsGitHub
  • hooks/gemini-install.jsGitHub
  • hooks/hermes-install.jsGitHub
  • hooks/install.jsGitHub
  • hooks/json-utils.jsGitHub
  • hooks/kimi-hook.jsGitHub
  • hooks/kimi-install.jsGitHub
  • hooks/kiro-hook.jsGitHub
  • hooks/kiro-install.jsGitHub
  • hooks/mimocode-install.jsGitHub
  • hooks/openclaw-install.jsGitHub
  • hooks/opencode-family-install.jsGitHub
  • hooks/opencode-family-jsonc.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/reasonix-hook.jsGitHub
  • hooks/reasonix-install.jsGitHub
  • hooks/server-config.jsGitHub
  • hooks/session-recovery-lease.jsGitHub
  • hooks/shared-process.jsGitHub
  • hooks/state-payload-size.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 69 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
5,885
Stars
600
Forks
Active
Maintenance
JavaScript
Language
AGPL-3.0
License
1h ago
Last commit
4mo ago
Created

Repo: rullerzhou-afk/clawd-on-desk