Skip to content
Development
Hook

Hooks

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

From plugin
piyaz
1999 skills10 agents8 hooks1 MCP

Claude Code

> /plugin marketplace add FrkAk/piyaz
> /plugin install piyaz@piyaz

Where it lives

  • hooks/useCopyToClipboard.tsGitHub
    Read the script
    import { useCallback, useEffect, useRef, useState } from "react";
    
    /** Copy-to-clipboard lifecycle state. */
    export type CopyStatus = "idle" | "copied" | "error";
    
    /**
     * Copy-to-clipboard state with auto-reset and error surfacing.
     *
     * Catches clipboard write failures (insecure context, Permissions-Policy,
     * unfocused document) and exposes them via the `'error'` status. Clears any
     * pending reset timer on unmount or rapid re-copy to avoid stale updates.
     *
     * @param resetMs - How long a non-idle status persists (default 1200ms).
     * @returns `{ status, copy }` — call `copy(text)` per invocation.
     */
    export function useCopyToClipboard(resetMs = 1200) {
      const [status, setStatus] = useState<CopyStatus>("idle");
      const timeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
    
      useEffect(() => {
        return () => {
          if (timeoutRef.current !== null) {
            clearTimeout(timeoutRef.current);
            timeoutRef.current = null;
          }
        };
      }, []);
    
      /**
       * Write `text` to the clipboard and update status.
       * @param text - String to write to the clipboard.
       * @returns Resolves once status has transitioned.
       */
      const copy = useCallback(
        async (text: string) => {
          if (timeoutRef.current !== null) {
            clearTimeout(timeoutRef.current);
            timeoutRef.current = null;
          }
          try {
            await navigator.clipboard.writeText(text);
            setStatus("copied");
          } catch {
            setStatus("error");
          }
          timeoutRef.current = setTimeout(() => {
            setStatus("idle");
            timeoutRef.current = null;
          }, resetMs);
        },
        [resetMs],
      );
    
      return { status, copy };
    }
    
  • hooks/useInlineEdit.tsGitHub
    Read the script
    "use client";
    
    import { useRef } from "react";
    import type {
      FocusEvent,
      KeyboardEvent as ReactKeyboardEvent,
      MouseEvent as ReactMouseEvent,
    } from "react";
    import {
      caretOffsetFromPoint,
      EDIT_HINT_LABEL,
      placeCaret,
    } from "@/components/shared/inlineEdit";
    
    /** Caret placement when entering edit mode. */
    type CaretMode = "point" | "end";
    
    /** Props spread onto the display element to make it an inline-edit trigger. */
    interface InlineEditTriggerProps {
      tabIndex: number;
      title: string;
      onDoubleClick: (event: ReactMouseEvent<HTMLElement>) => void;
      onKeyDown: (event: ReactKeyboardEvent<HTMLElement>) => void;
    }
    
    /** Handlers returned by `useInlineEdit` for wiring an inline-edit field. */
    interface InlineEditHandlers {
      /** Spread on the display element: double-click or Enter/Space enters edit mode. Keyboard activation only fires on the element itself, so focusable descendants (markdown links) stay reachable. */
      triggerProps: InlineEditTriggerProps;
      /** Bind to the touch-only edit button's `onClick`; enters edit mode with the caret at the end. */
      onActivate: () => void;
      /** Bind to the editor's `onFocus` to position the caret. */
      onEditorFocus: (
        event: FocusEvent<HTMLInputElement | HTMLTextAreaElement>,
      ) => void;
    }
    
    /**
     * Wire double-click and keyboard activation to edit, with caret positioning, for an inline field.
     * @param startEditing - Enters edit mode (sets the field's local editing state).
     * @param caret - `"point"` places the caret at the double-clicked character, for plain-text fields whose rendered text mirrors the editable value; `"end"` focuses at the end, for markdown fields and any activation that carries no pointer coordinate (keyboard, touch button).
     * @returns Trigger props for the display element, the touch-button activate handler, and the editor focus handler.
     */
    export function useInlineEdit(
      startEditing: () => void,
      caret: CaretMode = "end",
    ): InlineEditHandlers {
      const pendingCaret = useRef<number | null>(null);
    
      return {
        triggerProps: {
          tabIndex: 0,
          title: EDIT_HINT_LABEL,
          onDoubleClick: (event) => {
            pendingCaret.current =
              caret === "point"
                ? caretOffsetFromPoint(
                    event.currentTarget,
                    event.clientX,
                    event.clientY,
                  )
                : null;
            startEditing();
          },
          onKeyDown: (event) => {
            if (event.target !== event.currentTarget) return;
            if (event.key !== "Enter" && event.key !== " ") return;
            event.preventDefault();
            pendingCaret.current = null;
            startEditing();
          },
        },
        onActivate: () => {
          pendingCaret.current = null;
          startEditing();
        },
        onEditorFocus: (event) => {
          placeCaret(event.currentTarget, pendingCaret.current);
          pendingCaret.current = null;
        },
      };
    }
    
  • hooks/useMediaQuery.tsGitHub
    Read the script
    "use client";
    
    import { useCallback, useSyncExternalStore } from "react";
    
    /** No-op subscriber used on the server where `matchMedia` doesn't exist. */
    const noopSubscribe = () => () => {};
    
    /**
     * Track a CSS media query as boolean state. SSR returns the `defaultValue`
     * (defaults to `false`) and the real value lands on the first client paint.
     *
     * @param query - Standard CSS media query string (e.g. `(min-width: 1280px)`).
     * @param defaultValue - SSR fallback. Defaults to `false`.
     * @returns `true` when the query currently matches.
     */
    export function useMediaQuery(query: string, defaultValue = false): boolean {
      // `useSyncExternalStore` re-subscribes on identity change — memoise per `query`.
      const subscribe = useCallback(
        (callback: () => void) => {
          if (typeof window === "undefined") return noopSubscribe();
          const mql = window.matchMedia(query);
          mql.addEventListener("change", callback);
          return () => mql.removeEventListener("change", callback);
        },
        [query],
      );
    
      return useSyncExternalStore(
        subscribe,
        () =>
          typeof window === "undefined"
            ? defaultValue
            : window.matchMedia(query).matches,
        () => defaultValue,
      );
    }
    
  • hooks/useModalChrome.tsGitHub
    Read the script
    "use client";
    
    import { useEffect, useRef } from "react";
    import type React from "react";
    
    /** CSS selector matching tabbable descendants inside the dialog panel. */
    const FOCUSABLE_SELECTOR =
      'a[href], button:not([disabled]), textarea:not([disabled]), input:not([disabled]), select:not([disabled]), [tabindex]:not([tabindex="-1"])';
    
    /**
     * Keep keyboard focus inside the dialog panel while Tab is pressed.
     * @param event - The Tab keydown event.
     * @param panel - The panel element scoping focusable descendants.
     * @returns Nothing.
     */
    function trapTabFocus(event: KeyboardEvent, panel: HTMLElement | null): void {
      if (!panel) return;
      const focusables = panel.querySelectorAll<HTMLElement>(FOCUSABLE_SELECTOR);
      if (focusables.length === 0) return;
      const first = focusables[0];
      const last = focusables[focusables.length - 1];
      const active = document.activeElement as HTMLElement | null;
      if (event.shiftKey && active === first) {
        event.preventDefault();
        last.focus();
        return;
      }
      if (!event.shiftKey && active === last) {
        event.preventDefault();
        first.focus();
      }
    }
    
    /**
     * Active modal handle tracked by the global stack — capturing onClose
     * and panel bounds so the topmost dialog can handle Escape and trap Tab
     * focus, even when modals are nested.
     */
    interface ModalHandle {
      panelRef: React.RefObject<HTMLElement | null>;
      onClose: () => void;
    }
    
    const modalStack: ModalHandle[] = [];
    let globalListenerInstalled = false;
    let lockedBodyOverflow: string | null = null;
    
    /**
     * Lock background scrolling while any modal is open. Applies when the
     * first handle pushes onto the stack and restores the body's previous
     * overflow when the last one pops, so nested modals share one lock.
     */
    function syncScrollLock(): void {
      if (modalStack.length > 0 && lockedBodyOverflow === null) {
        lockedBodyOverflow = document.body.style.overflow;
        document.body.style.overflow = "hidden";
        return;
      }
      if (modalStack.length === 0 && lockedBodyOverflow !== null) {
        document.body.style.overflow = lockedBodyOverflow;
        lockedBodyOverflow = null;
      }
    }
    
    /**
     * Whether any dialog wired through {@link useModalChrome} is currently
     * open. Page-level Escape handlers (e.g. the detail header's
     * deselect-task listener) call this to yield to the open dialog instead
     * of racing it on listener registration order.
     *
     * @returns `true` while at least one modal is on the stack.
     */
    export function isModalOpen(): boolean {
      return modalStack.length > 0;
    }
    
    /**
     * Install the single document-level keydown listener responsible for
     * dispatching to the topmost active modal. Idempotent — installs once
     * and stays registered for the lifetime of the page so subsequent
     * modal mounts only push/pop the stack.
     */
    function ensureGlobalListenerInstalled(): void {
      if (globalListenerInstalled) return;
      globalListenerInstalled = true;
      document.addEventListener("keydown", (e) => {
        const top = modalStack[modalStack.length - 1];
        if (!top) return;
        if (e.key === "Escape") {
          e.preventDefault();
          e.stopPropagation();
          top.onClose();
          return;
        }
        if (e.key === "Tab") {
          trapTabFocus(e, top.panelRef.current);
        }
      });
    }
    
    /**
     * Wires modal chrome behavior: Escape to close, Tab focus trap, focus
     * restore on unmount, and a background scroll lock while open — the
     * behavior `aria-modal` promises. Stack-aware so nested modals (e.g. a
     * destructive confirm dialog opened from inside a settings modal) are
     * each handled by the topmost dialog only — outer modals stay open
     * until the inner one dismisses.
     *
     * @param open - Whether the modal is currently open.
     * @param onClose - Callback invoked when Escape pops this modal off
     *   the stack. Closure identity may change across renders; the hook
     *   always dispatches the latest `onClose`.
     * @param panelRef - Ref to the modal panel — used to bound the focus
     *   trap and to seed initial focus.
     */
    export function useModalChrome(
      open: boolean,
      onClose: () => void,
      panelRef: React.RefObject<HTMLElement | null>,
    ): void {
      const previousFocusRef = useRef<HTMLElement | null>(null);
      const onCloseRef = useRef(onClose);
      useEffect(() => {
        onCloseRef.current = onClose;
      }, [onClose]);
    
      useEffect(() => {
        if (!open) return;
        ensureGlobalListenerInstalled();
        previousFocusRef.current =
          (document.activeElement as HTMLElement | null) ?? null;
    
        const handle: ModalHandle = {
          panelRef,
          onClose: () => onCloseRef.current(),
        };
        modalStack.push(handle);
        syncScrollLock();
    
        const frame = window.requestAnimationFrame(() => {
          const panel = panelRef.current;
          if (!panel) return;
          const focusable = panel.querySelector<HTMLElement>(
            'input, textarea, select, button, [tabindex]:not([tabindex="-1"])',
          );
          focusable?.focus();
        });
    
        return () => {
          window.cancelAnimationFrame(frame);
          const idx = modalStack.indexOf(handle);
          if (idx !== -1) modalStack.splice(idx, 1);
          syncScrollLock();
          previousFocusRef.current?.focus?.();
        };
      }, [open, panelRef]);
    }
    
  • hooks/useMounted.tsGitHub
    Read the script
    "use client";
    
    import { useSyncExternalStore } from "react";
    
    /** No-op subscribe: the mount snapshot never changes after hydration. */
    const subscribe = () => () => {};
    
    /**
     * Detect the first client render. Returns `false` on the server and during
     * the hydration paint, then `true` once mounted on the client, without a
     * setState-in-effect. Use to defer viewport-dependent layout past hydration
     * so it never flashes an SSR-default layout.
     *
     * @returns `true` after the component has mounted on the client.
     */
    export function useMounted(): boolean {
      return useSyncExternalStore(
        subscribe,
        () => true,
        () => false,
      );
    }
    
  • hooks/useNotesCollapse.tsGitHub
    Read the script
    "use client";
    
    import { useCallback, useSyncExternalStore } from "react";
    
    /** Cookie max-age in seconds (1 year). */
    const COOKIE_MAX_AGE = 60 * 60 * 24 * 365;
    
    interface CookieCollapse {
      /** Whether the surface is currently collapsed (hidden). */
      collapsed: boolean;
      /** Flip the collapsed state and persist it. */
      toggle: () => void;
    }
    
    /**
     * Build a cookie-persisted collapse hook around one cookie name. Each
     * call owns its module-level store (listener set plus lazily-read cached
     * value), so repeated `getSnapshot` calls stay referentially stable.
     * Server-renders expanded ({@link useSyncExternalStore} server snapshot),
     * then reconciles to the cookie value after hydration.
     *
     * @param cookieName - Cookie persisting the preference.
     * @returns Hook exposing the collapse state and its toggle.
     */
    function createCookieCollapse(cookieName: string): () => CookieCollapse {
      const listeners = new Set<() => void>();
      let cachedValue: boolean | null = null;
    
      /**
       * Read the persisted value from `document.cookie`. Browser-only.
       *
       * @returns `true` when the cookie marks the surface as collapsed.
       */
      function readCookie(): boolean {
        try {
          const match = document.cookie.match(
            new RegExp(`(?:^|; )${cookieName}=([^;]*)`),
          );
          return match?.[1] === "1";
        } catch {
          return false;
        }
      }
    
      /**
       * Write the persisted value to `document.cookie`. Browser-only.
       *
       * @param next - The new collapse state.
       */
      function writeCookie(next: boolean): void {
        try {
          document.cookie = `${cookieName}=${next ? "1" : "0"}; path=/; max-age=${COOKIE_MAX_AGE}; samesite=lax`;
        } catch {
          /* swallow cookie errors; preference is non-critical */
        }
      }
    
      /**
       * Subscribe to in-tab collapse changes.
       *
       * @param onStoreChange - Notification callback from {@link useSyncExternalStore}.
       * @returns Unsubscribe function.
       */
      function subscribe(onStoreChange: () => void): () => void {
        listeners.add(onStoreChange);
        return () => {
          listeners.delete(onStoreChange);
        };
      }
    
      /**
       * Read the cached collapse state, lazily loading from the cookie on
       * first access.
       *
       * @returns `true` when the surface should render collapsed.
       */
      function getClientSnapshot(): boolean {
        if (cachedValue !== null) return cachedValue;
        cachedValue = readCookie();
        return cachedValue;
      }
    
      return function useCookieCollapse(): CookieCollapse {
        const collapsed = useSyncExternalStore(
          subscribe,
          getClientSnapshot,
          () => false,
        );
        const toggle = useCallback(() => {
          cachedValue = !getClientSnapshot();
          writeCookie(cachedValue);
          listeners.forEach((l) => l());
        }, []);
        return { collapsed, toggle };
      };
    }
    
    /** Cookie-persisted toggle for hiding the notes tree rail at `lg` and up. */
    export const useNotesRailCollapse = createCookieCollapse(
      "piyaz-notes-rail-collapsed",
    );
    
    /** Cookie-persisted toggle for hiding the notes settings ribbon at `xl` and up. */
    export const useNotesSettingsCollapse = createCookieCollapse(
      "piyaz-notes-settings-collapsed",
    );
    
  • hooks/usePopoverAnchor.tsGitHub
  • hooks/useSkeletonVisibility.tsGitHub

All 8 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.

@frkak
Ships withpiyaz

The agentic workspace where people and agents work together in the loop.

1999 skills10 agents1 MCP
Get the whole plugin

Stars on GitHub

Momentum

stars in total
199
Star history of FrkAk/piyaz on GitHub, a chart by star-history.com

More plugins that ship hooks.

superpowers

superpowers

A development methodology for coding agents that makes the agent brainstorm before building, follow plans, run parallel work and finish branches in a disciplined way. You get fewer skipped steps and more consistent results from your coding assistant.

15 skills1 hook311k
@obra@obra
Install Plugin
ponytail

ponytail

A Claude Code plugin with skills for auditing a whole repo, reviewing a change, listing deferred shortcut comments, and showing measured savings and a quick command reference.

6 skills10 hooks188k
@dietrichgebert@dietrichgebert
Install Plugin
agent-skills

agent-skills

Walks a software change from a written spec through design, testing, code review, CI/CD, and release, applying the matching engineering skill at each stage. Useful for developers using Claude Code on full development workflows.

25 skills+3112k
@addyosmani@addyosmani
Install Plugin
planning-with-files

planning-with-files

Persistent file-based planning for AI coding agents on long-running, multi-step tasks. It keeps task_plan.md, findings.md and progress.md on disk, so work can resume after /clear or context compaction.

6 skills+228k
@othmanadi@othmanadi
Install Plugin
gsd-core

gsd-core

A spec-driven development workflow for Claude Code that guides a project through planning, phases, milestone audits and fixes. Includes skills to generate tests from UAT criteria and to audit completed work.

72 skills+311k
@open-gsd@open-gsd
Install Plugin
superpowers-zh

superpowers-zh

Chinese-language skill pack for Claude Code that adds workflow guidance for brainstorming, TDD, debugging, and code review, plus Chinese conventions for commits, documentation, and domestic Git platforms like Gitee and Coding.net.

21 skills1 hook8.5k
@jnmetacode@jnmetacode
Install Plugin
socraticode

socraticode

Gives Claude Code semantic search, dependency graph and architecture context over a codebase, so an AI assistant can explore and understand a project's code. Includes skills for indexing a project and managing its file watching.

2 skills+33.4k
@giancarloerra@giancarloerra
Install Plugin
cocoindex-code

cocoindex-code

A lightweight code search engine that uses AST-based semantic search with local embeddings and incremental indexing. It helps Claude Code users find code faster and more efficiently.

1 skill1 MCP+13k
@cocoindex-io@cocoindex-io
Install Plugin

Questions about the hooks in piyaz.

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