Skip to content
Agent Orchestration
Hook

Hooks

What goodable runs automatically, and when. A hook is a command Claude Code fires at a fixed moment, without you asking for it.

From plugin
goodable
19510 skills3 hooks

Claude Code

$ npx -y skills add imgoodbai/goodable --agent claude-code

Where it lives

  • hooks/useCLI.tsGitHub
    Read the script
    /**
     * CLI Hook
     * Manages CLI configuration and status
     */
    import { useState, useCallback, useEffect } from 'react';
    import { CLIOption, CLIStatus, CLIPreference, CLI_OPTIONS } from '@/types/cli';
    import { getDefaultModelForCli } from '@/lib/constants/cliModels';
    import { DEFAULT_ACTIVE_CLI, normalizeModelForCli, sanitizeActiveCli } from '@/lib/utils/cliOptions';
    
    interface UseCLIOptions {
      projectId: string;
    }
    
    const buildOptimisticStatus = (): CLIStatus =>
      CLI_OPTIONS.reduce((acc, option) => {
        acc[option.id] = {
          installed: true,
          checking: false,
          available: true,
          configured: true,
          models: option.models?.map((model) => model.id),
        };
        return acc;
      }, {} as CLIStatus);
    
    export const createCliStatusFallback = (): CLIStatus => buildOptimisticStatus();
    
    export async function fetchCliStatusSnapshot(): Promise<CLIStatus> {
      const API_BASE = process.env.NEXT_PUBLIC_API_BASE ?? '';
      try {
        const response = await fetch(`${API_BASE}/api/settings/cli-status`);
        if (!response.ok) {
          throw new Error(`Failed to fetch CLI status: ${response.status}`);
        }
    
        const payload = (await response.json()) as CLIStatus;
        const optimistic = buildOptimisticStatus();
    
        for (const option of CLI_OPTIONS) {
          const entry = payload[option.id];
          if (!entry) {
            continue;
          }
          optimistic[option.id] = {
            ...optimistic[option.id],
            ...entry,
            checking: false,
            available: entry.available ?? entry.installed ?? optimistic[option.id]?.available ?? false,
            configured: entry.configured ?? entry.installed ?? optimistic[option.id]?.configured ?? false,
            models: entry.models ?? option.models?.map((model) => model.id),
          };
        }
    
        return optimistic;
      } catch (error) {
        console.warn('Failed to fetch CLI status from API:', error);
        return buildOptimisticStatus();
      }
    }
    
    export function useCLI({ projectId }: UseCLIOptions) {
      const [cliOptions, setCLIOptions] = useState<CLIOption[]>(() => CLI_OPTIONS.map((option) => ({ ...option })));
      const [preference, setPreference] = useState<CLIPreference | null>(null);
      const [statuses, setStatuses] = useState<CLIStatus>(() => createCliStatusFallback());
      const [isLoading, setIsLoading] = useState(false);
    
      const parsePreference = useCallback((payload: unknown): CLIPreference => {
        const data = payload as Record<string, unknown> | null | undefined;
    
        const preferredRaw =
          typeof data?.preferredCli === 'string'
            ? data.preferredCli
            : typeof data?.preferred_cli === 'string'
            ? data.preferred_cli
            : DEFAULT_ACTIVE_CLI;
    
        const preferredCli = sanitizeActiveCli(preferredRaw, DEFAULT_ACTIVE_CLI);
    
        const fallbackEnabled =
          typeof data?.fallbackEnabled === 'boolean'
            ? data.fallbackEnabled
            : typeof data?.fallback_enabled === 'boolean'
            ? data.fallback_enabled
            : false;
    
        const rawModel =
          typeof data?.selectedModel === 'string'
            ? data.selectedModel
            : typeof data?.selected_model === 'string'
            ? data.selected_model
            : undefined;
        const normalizedModel = normalizeModelForCli(preferredCli, rawModel, preferredCli);
    
        return {
          preferredCli,
          fallbackEnabled,
          selectedModel: normalizedModel || getDefaultModelForCli(preferredCli),
        };
      }, []);
    
      // Load CLI preference
      const loadPreference = useCallback(async () => {
        try {
          const API_BASE = process.env.NEXT_PUBLIC_API_BASE ?? '';
          const response = await fetch(`${API_BASE}/api/projects/${projectId}`);
          if (!response.ok) {
            throw new Error('Failed to load project preferences');
          }
    
          const payload = await response.json();
        const project = payload?.data ?? payload ?? {};
        setPreference(parsePreference(project));
      } catch (error) {
        console.error('Failed to load CLI preference:', error);
        setPreference({
          preferredCli: DEFAULT_ACTIVE_CLI,
          fallbackEnabled: false,
          selectedModel: getDefaultModelForCli(DEFAULT_ACTIVE_CLI),
        });
      }
    }, [projectId, parsePreference]);
    
      const applyStatusToState = useCallback((status: CLIStatus) => {
        setStatuses(status);
        setCLIOptions(
          CLI_OPTIONS.map((option) => {
            const entry = status[option.id];
            return {
              ...option,
              available: Boolean(entry?.available ?? entry?.installed ?? option.available),
              configured: Boolean(entry?.configured ?? entry?.installed ?? option.configured),
            };
          })
        );
      }, []);
    
      // Load all CLI statuses
      const loadStatuses = useCallback(async () => {
        try {
          setIsLoading(true);
          const status = await fetchCliStatusSnapshot();
          applyStatusToState(status);
        } finally {
          setIsLoading(false);
        }
      }, [applyStatusToState]);
    
      // Check single CLI status
      const checkCLIStatus = useCallback(async (cliType: string) => {
        const status = await fetchCliStatusSnapshot();
        applyStatusToState(status);
        return status[cliType];
      }, [applyStatusToState]);
    
      // Update CLI preference
      const updatePreference = useCallback(async (preferredCliInput: string) => {
        const sanitizedInput = sanitizeActiveCli(preferredCliInput, DEFAULT_ACTIVE_CLI);
        try {
          const API_BASE = process.env.NEXT_PUBLIC_API_BASE ?? '';
          const response = await fetch(`${API_BASE}/api/projects/${projectId}`, {
            method: 'PUT',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({ preferredCli: sanitizedInput }),
          });
    
          if (!response.ok) throw new Error('Failed to update CLI preference');
    
          const payload = await response.json();
          const project = payload?.data ?? payload ?? {};
    
          const responseCli = sanitizeActiveCli(
            project.preferredCli ?? project.preferred_cli ?? sanitizedInput,
            DEFAULT_ACTIVE_CLI
          );
          const rawSelected = project.selectedModel ?? project.selected_model;
    
          setPreference(
  • hooks/useSlimMode.tsGitHub
    Read the script
    "use client";
    
    import { useState, useEffect } from 'react';
    
    const SLIM_MODE_KEY = 'goodable_slim_mode';
    const SLIM_PREVIOUS_SIZE_KEY = 'goodable_slim_previous_size';
    const SLIM_MODE_EVENT = 'goodable-slim-mode-changed';
    
    const SLIM_WIDTH = 420;
    const SLIM_HEIGHT = 550;
    const DEFAULT_WIDTH = 1280;
    const DEFAULT_HEIGHT = 800;
    const NORMAL_MIN_WIDTH = 1024;
    const NORMAL_MIN_HEIGHT = 640;
    
    /**
     * Global hook for managing slim mode state.
     * Slim mode only controls window size - layout adapts via CSS media queries.
     */
    export function useSlimMode() {
      const [isSlimMode, setIsSlimMode] = useState(false);
      const [isInitialized, setIsInitialized] = useState(false);
    
      // Initialize from localStorage
      useEffect(() => {
        if (typeof window === 'undefined') return;
    
        const saved = localStorage.getItem(SLIM_MODE_KEY);
        if (saved === 'true') {
          setIsSlimMode(true);
        }
        setIsInitialized(true);
      }, []);
    
      // Listen for toggle event from title bar
      useEffect(() => {
        if (typeof window === 'undefined') return;
    
        const handleTitleBarToggle = async () => {
          const desktopAPI = (window as any).desktopAPI;
          const currentSlimMode = localStorage.getItem(SLIM_MODE_KEY) === 'true';
          const newSlimMode = !currentSlimMode;
    
          // Update localStorage and state
          localStorage.setItem(SLIM_MODE_KEY, String(newSlimMode));
          setIsSlimMode(newSlimMode);
    
          // Dispatch event for other listeners (like preload.js button state)
          window.dispatchEvent(new CustomEvent(SLIM_MODE_EVENT, { detail: { isSlimMode: newSlimMode } }));
    
          // Handle window resize
          if (desktopAPI?.windowControls?.setSize && desktopAPI?.windowControls?.getSize) {
            try {
              if (newSlimMode) {
                // Entering slim mode: save current size to localStorage and resize
                const currentSize = await desktopAPI.windowControls.getSize();
                if (currentSize.width > 500) {
                  localStorage.setItem(SLIM_PREVIOUS_SIZE_KEY, JSON.stringify(currentSize));
                  await desktopAPI.windowControls.setSize({
                    width: SLIM_WIDTH,
                    height: SLIM_HEIGHT,
                    minWidth: 400,
                    minHeight: 400
                  });
                }
              } else {
                // Exiting slim mode: restore previous size from localStorage
                const savedSize = localStorage.getItem(SLIM_PREVIOUS_SIZE_KEY);
                if (savedSize) {
                  const previousSize = JSON.parse(savedSize);
                  await desktopAPI.windowControls.setSize({
                    width: previousSize.width,
                    height: previousSize.height,
                    minWidth: NORMAL_MIN_WIDTH,
                    minHeight: NORMAL_MIN_HEIGHT
                  });
                  localStorage.removeItem(SLIM_PREVIOUS_SIZE_KEY);
                } else {
                  // No previous size, restore to default
                  await desktopAPI.windowControls.setSize({
                    width: DEFAULT_WIDTH,
                    height: DEFAULT_HEIGHT,
                    minWidth: NORMAL_MIN_WIDTH,
                    minHeight: NORMAL_MIN_HEIGHT
                  });
                }
              }
            } catch (e) {
              console.error('Failed to resize window:', e);
            }
          }
        };
    
        window.addEventListener('electron-toggle-slim-mode', handleTitleBarToggle);
    
        return () => {
          window.removeEventListener('electron-toggle-slim-mode', handleTitleBarToggle);
        };
      }, []);
    
      // Listen for changes from other tabs/windows
      useEffect(() => {
        if (typeof window === 'undefined') return;
    
        const handleStorageChange = (e: StorageEvent) => {
          if (e.key === SLIM_MODE_KEY) {
            setIsSlimMode(e.newValue === 'true');
          }
        };
    
        const handleCustomEvent = (e: Event) => {
          const customEvent = e as CustomEvent<{ isSlimMode: boolean }>;
          setIsSlimMode(customEvent.detail.isSlimMode);
        };
    
        window.addEventListener('storage', handleStorageChange);
        window.addEventListener(SLIM_MODE_EVENT, handleCustomEvent);
    
        return () => {
          window.removeEventListener('storage', handleStorageChange);
          window.removeEventListener(SLIM_MODE_EVENT, handleCustomEvent);
        };
      }, []);
    
      return {
        isSlimMode,
        isInitialized,
      };
    }
    
    /**
     * Simplified hook - just returns slim mode state.
     * Window size is now handled within useSlimMode itself.
     */
    export function useSlimModeWindowSize() {
      const { isSlimMode } = useSlimMode();
      return { isSlimMode };
    }
    
  • hooks/useWebSocket.tsGitHub
    Read the script
    /**
     * WebSocket Hook
     * Manages WebSocket connection for real-time updates
     */
    import { useEffect, useRef, useCallback, useState } from 'react';
    import { WEBSOCKET_CONFIG } from '@/lib/config/constants';
    import type { ChatMessage, RealtimeEvent, RealtimeStatus } from '@/types';
    
    interface WebSocketOptions {
      projectId: string;
      onMessage?: (message: ChatMessage) => void;
      onStatus?: (status: string, data?: RealtimeStatus | Record<string, unknown>, requestId?: string) => void;
      onConnect?: () => void;
      onDisconnect?: () => void;
      onError?: (error: Error) => void;
    }
    
    export function useWebSocket({
      projectId,
      onMessage,
      onStatus,
      onConnect,
      onDisconnect,
      onError
    }: WebSocketOptions) {
      const wsRef = useRef<WebSocket | null>(null);
      const reconnectTimeoutRef = useRef<NodeJS.Timeout | null>(null);
      const heartbeatIntervalRef = useRef<NodeJS.Timeout | null>(null);
      const connectionAttemptsRef = useRef(0);
      const shouldReconnectRef = useRef(true);
      const manualCloseRef = useRef(false);
      const [isConnected, setIsConnected] = useState(false);
      const [isConnecting, setIsConnecting] = useState(false);
      const handlersRef = useRef({
        onMessage,
        onStatus,
        onConnect,
        onDisconnect,
        onError,
      });
    
      useEffect(() => {
        handlersRef.current = {
          onMessage,
          onStatus,
          onConnect,
          onDisconnect,
          onError,
        };
      }, [onMessage, onStatus, onConnect, onDisconnect, onError]);
    
      const clearHeartbeat = useCallback(() => {
        if (heartbeatIntervalRef.current) {
          clearInterval(heartbeatIntervalRef.current);
          heartbeatIntervalRef.current = null;
        }
      }, []);
    
      const startHeartbeat = useCallback(() => {
        clearHeartbeat();
        heartbeatIntervalRef.current = setInterval(() => {
          const socket = wsRef.current;
          if (!socket || socket.readyState !== WebSocket.OPEN) {
            return;
          }
          try {
            socket.send('ping');
          } catch (error) {
            console.error('Failed to send WebSocket ping:', error);
          }
        }, 25000);
      }, [clearHeartbeat]);
    
      const connect = useCallback(() => {
        const existing = wsRef.current;
        if (existing) {
          if (
            existing.readyState === WebSocket.OPEN ||
            existing.readyState === WebSocket.CONNECTING
          ) {
            return;
          }
    
          try {
            existing.close(1000, 'Reconnecting');
          } catch {
            // Ignore close errors; we'll replace the socket below.
          }
          wsRef.current = null;
        }
    
        // Don't reconnect if we're intentionally disconnecting
        if (!shouldReconnectRef.current) {
          return;
        }
    
        const resolveWebSocketUrl = () => {
          const rawBase = process.env.NEXT_PUBLIC_WS_BASE?.trim() ?? '';
          const endpoint = `/api/ws/${projectId}`;
          if (rawBase.length > 0) {
            const normalizedBase = rawBase.replace(/\/+$/, '');
            return `${normalizedBase}${endpoint}`;
          }
          if (typeof window !== 'undefined') {
            const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
            return `${protocol}//${window.location.host}${endpoint}`;
          }
          throw new Error('WebSocket base URL is not available');
        };
    
        const resolveHttpWarmupUrl = () => {
          const rawBase = process.env.NEXT_PUBLIC_WS_BASE?.trim() ?? '';
          const endpoint = `/api/ws/${projectId}`;
          if (rawBase.length > 0) {
            // Convert ws/wss to http/https for the warm-up fetch
            const normalizedBase = rawBase
              .replace(/\/+$/, '')
              .replace(/^ws:\/\//i, 'http://')
              .replace(/^wss:\/\//i, 'https://');
            return `${normalizedBase}${endpoint}`;
          }
          if (typeof window !== 'undefined') {
            const httpProto = window.location.protocol === 'https:' ? 'https:' : 'http:';
            return `${httpProto}//${window.location.host}${endpoint}`;
          }
          throw new Error('HTTP base URL is not available');
        };
    
        const openWebSocket = () => {
          setIsConnecting(true);
          const ws = new WebSocket(resolveWebSocketUrl());
          manualCloseRef.current = false;
    
          ws.onopen = () => {
            setIsConnected(true);
            setIsConnecting(false);
            connectionAttemptsRef.current = 0;
            startHeartbeat();
            handlersRef.current.onConnect?.();
          };
    
          ws.onmessage = (event) => {
            if (event.data === 'pong') {
              return;
            }
    
            try {
              const envelope = JSON.parse(event.data) as RealtimeEvent;
              const { onMessage: handleMessage, onStatus: handleStatus, onError: handleError } =
                handlersRef.current;
    
              switch (envelope.type) {
                case 'message':
                  if (envelope.data && handleMessage) {
                    handleMessage(envelope.data);
                  }
                  break;
                case 'status':
                  if (envelope.data && handleStatus) {
                    handleStatus(envelope.data.status, envelope.data, envelope.data.requestId);
                  }
                  break;
                case 'preview_status':
                  console.log(`====安装预览 ### [frontend] preview_status event received, status=${envelope.data?.status}`, envelope.data);
                  if (envelope.data && handleStatus) {
                    handleStatus(envelope.data.status, envelope.data, envelope.data.requestId);
                  }
                  break;
                case 'error': {
                  const message = envelope.error ?? 'Realtime bridge error';
                  const rawData = envelope.data as Record<string, unknown> | undefined;
                  const requestId = (() => {
                    if (!rawData) return undefined;
                    const direct = rawData.requestId ?? rawData.request_id;
                    return typeof direct === 'string' ? direct : undefined;
                  })();
                  const payload: RealtimeStatus = {
                    status: 'error',
                    message,
                    ...(requestId ? { requestId } : {}),
                  };
                  handleStatus?.('error', pay

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.

@imgoodbai
Ships withgoodable

Goodable is a local-first Desktop AI Workspace (Desktop Agent Runtime) powered by Claude Agent SDK, combining OS/file control, browser automation, and coding into composable Skills (tool + app modes) to build, run, and publish real AI apps.

Stars on GitHub

Momentum

stars in total
195
Star history of imgoodbai/goodable on GitHub, a chart by star-history.com

More plugins that ship hooks.

oh-my-claudecode
Plugin

oh-my-claudecode

Splits a complex task across several specialized Claude Code agents working in parallel, then keeps going until the result is checked. You describe what you want in plain language instead of memorizing commands.

48 skills+440k
@yeachan-heo@yeachan-heo
Install Plugin
ouroboros
Plugin

ouroboros

Turns a rough feature idea into a written spec through guided questions, then has the agent build it and checks the result against that spec. Instead of hand-checking whether the work is finished, you get an automatic evaluation gate that runs in your coding agent.

25 skills+36.3k
@q00@q00
Install Plugin
stellarlinkco-myclaude
Plugin

stellarlinkco-myclaude

A plugin that runs coding tasks through multiple AI backends (Codex, Claude, Gemini, OpenCode) with a structured development workflow, parallel execution, browser automation and multi-session task checkpointing.

11 skills+32.7k
@stellarlinkco@stellarlinkco
Install Plugin
harmonist
Plugin

harmonist

Coordinates AI agents with mechanical protocol enforcement, a workflow gate, memory validation and a session hook. Includes a catalog of specialist agents and has no runtime dependencies.

199 agents+12.3k
@gammalabtechnologies@gammalabtechnologies
Install Plugin
bernstein
Plugin

bernstein

Bernstein is a Claude Code plugin for orchestrating AI agents. It adds skills to list and inspect agents, review and approve tasks, create tasks, track costs and budgets, and show alerts for failed or stalled work.

26 skills+41.3k
@sipyourdrink-ltd@sipyourdrink-ltd
Install Plugin
fablize
Plugin

fablize

A Claude Code plugin that enforces finishing multi-step tasks with evidence and verification at each stage. It works as a procedure for Opus or any Claude model, not as chance.

1 skill+2894
@fivetaku@fivetaku
Install Plugin
hello2cc
Plugin

hello2cc

Adjusts how third-party models behave inside Claude Code so they run closer to a native Opus session, covering subagent lifecycle, session hooks and tool interception. It does not replace your gateway, provider mapping or account setup.

1 agent11 hooks689
@hellowind777@hellowind777
Install Plugin
ww-w-ai-bkit-claude-code
Plugin

ww-w-ai-bkit-claude-co…

Runs a PDCA workflow with phase gating, spec checks, and delegation to sub-agents for planning, QA, and deployment. Fires at session start and on prompts, edits, task creation, and phase transitions.

44 skills+3606
@ww-w-ai@ww-w-ai
Install Plugin

Questions about the hooks in goodable.

Which coding agents can use the hooks in goodable?
The hooks in goodable install in Claude Code and Cursor: each runs Claude Code hooks from .claude/settings.json, so copy the hooks there.