Skip to content
AI & Agents
Hook

Hooks

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

From plugin
allium
5217 skills5 agents2 hooks

Claude Code

$ npx -y skills add juxt/allium --agent claude-code

What fires, and when

PreToolUse

  • MatchesTask|Agentnode "${CLAUDE_PLUGIN_ROOT}/hooks/loop-trace.mjs" pre

PostToolUse

  • MatchesWrite|Editnode "${CLAUDE_PLUGIN_ROOT}/hooks/allium-check.mjs"
  • MatchesTask|Agentnode "${CLAUDE_PLUGIN_ROOT}/hooks/loop-trace.mjs" post
Read hooks/hooks.json

Where it lives

  • hooks/allium-check.mjsRunsGitHub
    Read the script
    import { execFileSync } from "child_process";
    import { realpathSync, statSync, existsSync, mkdirSync, writeFileSync } from "fs";
    import { homedir } from "os";
    import path from "path";
    
    process.on("uncaughtException", () => process.exit(0));
    
    // Per-machine marker recording that the install notice has been shown once.
    // Lives in the user's cache dir so it spans every project and every spec on
    // this machine — once installed, the CLI is on PATH for all of them anyway.
    function installNoticeMarkerPath() {
      const cacheHome = process.env.XDG_CACHE_HOME || path.join(homedir(), ".cache");
      return path.join(cacheHome, "allium", "cli-install-notice-shown");
    }
    
    // Fallback marker in the project root, used when the per-machine cache dir
    // isn't writable. Scoped to one project rather than the whole machine, but it
    // still stops the notice from re-firing on every edit.
    function projectNoticeMarkerPath(projectRoot) {
      return path.join(projectRoot, ".allium-cli-notice-shown");
    }
    
    function markerExists(p) {
      try {
        return existsSync(p);
      } catch {
        return false;
      }
    }
    
    function persistMarker(p) {
      try {
        mkdirSync(path.dirname(p), { recursive: true });
        writeFileSync(p, "Allium CLI install notice shown.\n");
        return true;
      } catch {
        return false;
      }
    }
    
    function installCommandFor(platform) {
      if (platform === "darwin") {
        return "brew tap juxt/allium && brew install allium   (or: cargo install allium-cli)";
      }
      if (platform === "win32") {
        return "cargo install allium-cli";
      }
      return "cargo install allium-cli   (or, with Homebrew: brew tap juxt/allium && brew install allium)";
    }
    
    // On the first .allium edit where the CLI is absent, surface a one-time notice
    // instructing the model to offer to install it. Returns true if the notice was
    // emitted (caller should exit non-zero so the hook framework forwards stderr to
    // the model), false if it was already shown before (caller should exit cleanly).
    function showInstallNoticeOnce(projectRoot) {
      const cacheMarker = installNoticeMarkerPath();
      const projectMarker = projectNoticeMarkerPath(projectRoot);
    
      // If either marker is already present, the user has been notified before.
      if (markerExists(cacheMarker) || markerExists(projectMarker)) return false;
    
      // Persist the per-machine marker first; if its cache dir isn't writable, fall
      // back to a marker in the project root. Knowing which (if any) succeeded lets
      // the notice tell the user, honestly, whether it will recur.
      let savedTo = null;
      if (persistMarker(cacheMarker)) savedTo = "cache";
      else if (persistMarker(projectMarker)) savedTo = "project";
    
      const command = installCommandFor(process.platform);
      const lines = [
        "Allium CLI not found on PATH — this spec is NOT being formally checked.",
        "",
        "The CLI (`allium check` / `allium analyse`) adds structural verification the",
        "language model can't do reliably alone: tracing data flow across rules, proving",
        "every entity lifecycle can reach a terminal state, and spotting dead ends. With",
        "it installed, every .allium edit is checked and the results feed back into the",
        "conversation, producing more correct and complete specs.",
        "",
        "ACTION: Offer to install it for the user now. If they agree, first check the",
        "relevant package manager exists, then run the command below.",
        "",
        `  Suggested command for this machine (platform: ${process.platform}):`,
        `    ${command}`,
        "  More detail: https://github.com/juxt/allium-tools — make sure the `allium`",
        "  binary ends up on your PATH after installing.",
      ];
    
      if (savedTo === "cache") {
        lines.push(
          "",
          "This notice fires only once per machine, so do not raise it again afterwards.",
        );
      } else if (savedTo === "project") {
        lines.push(
          "",
          "NOTE: the per-machine marker couldn't be written, so this has been recorded",
          `in the project instead (${projectMarker}). The notice won't fire again for`,
          "this project; let the user know they may want to add that file to .gitignore.",
        );
      } else {
        // Neither marker could be saved, so the hook can't remember it has notified
        // the user. Be upfront about that and hand off to manual install.
        lines.push(
          "",
          "NOTE: the notice marker could NOT be saved — neither the per-machine cache",
          `nor the project root (${projectRoot}) is writable — so this would otherwise`,
          "reappear on every .allium edit. Tell the user this directly, share the manual",
          "install steps above, and ask them to confirm they're happy to install the CLI",
          "themselves. Once they confirm, continue with their task without blocking, and",
          "treat the missing CLI as an acknowledged limitation rather than re-raising it",
          "each edit until the `allium` binary is on PATH.",
        );
      }
    
      process.stderr.write(lines.join("\n") + "\n");
      return true;
    }
    
    let data = "";
    for await (const chunk of process.stdin) {
      data += chunk;
    }
    
    let input;
    try {
      input = JSON.parse(data);
    } catch {
      process.exit(0);
    }
    // Claude Code sends { tool_input: { file_path } };
    // Cursor sends { file_path, workspace_roots };
    // Windsurf sends { tool_info: { file_path } }.
    const filePath = input.tool_input?.file_path ?? input.file_path ?? input.tool_info?.file_path;
    
    if (typeof filePath !== "string" || path.extname(filePath) !== ".allium") {
      process.exit(0);
    }
    
    let resolved;
    try {
      resolved = realpathSync(filePath);
      if (!statSync(resolved).isFile()) process.exit(0);
    } catch {
      process.exit(0);
    }
    
    // Claude Code sets CLAUDE_PROJECT_ROOT; Cursor provides workspace_roots in the payload.
    const payloadRoots = Array.isArray(input.workspace_roots) ? input.workspace_roots : [];
    const roots = [process.env.CLAUDE_PROJECT_ROOT, ...payloadRoots].filter(Boolean);
    if (roots.length === 0) roots.push(process.cwd());
    
    const resolvedRoots = [];
    for (const r of roots) {
      try {
        resolvedRoots.push(realpathSync(r));
      } catch {
        // 
  • hooks/allium-check.test.mjsGitHub
    Read the script
    import { execFileSync } from "child_process";
    import { mkdtempSync, mkdirSync, writeFileSync, symlinkSync, rmSync, existsSync, chmodSync } from "fs";
    import path from "path";
    import { tmpdir } from "os";
    
    const hook = new URL("./allium-check.mjs", import.meta.url).pathname;
    let passed = 0;
    let failed = 0;
    
    function run(input, env = {}) {
      try {
        execFileSync("node", [hook], {
          input: JSON.stringify(input),
          encoding: "utf-8",
          stdio: ["pipe", "pipe", "pipe"],
          env: { ...process.env, ...env },
        });
        return { status: 0, stderr: "" };
      } catch (e) {
        return { status: e.status, stderr: e.stderr || "" };
      }
    }
    
    function assert(name, actual, expected) {
      if (actual === expected) {
        console.log(`  pass: ${name}`);
        passed++;
      } else {
        console.log(`  FAIL: ${name} (expected ${expected}, got ${actual})`);
        failed++;
      }
    }
    
    // Set up fixtures
    const projectRoot = mkdtempSync(path.join(tmpdir(), "allium-hook-test-"));
    const validFile = path.join(projectRoot, "test.allium");
    writeFileSync(validFile, "-- allium: 3\n");
    const invalidFile = path.join(projectRoot, "bad.allium");
    writeFileSync(invalidFile, "this is not valid allium\n");
    
    const subDir = path.join(projectRoot, "specs", "nested");
    mkdirSync(subDir, { recursive: true });
    const nestedFile = path.join(subDir, "deep.allium");
    writeFileSync(nestedFile, "-- allium: 3\n");
    
    const outsideDir = mkdtempSync(path.join(tmpdir(), "allium-hook-outside-"));
    const outsideFile = path.join(outsideDir, "evil.allium");
    writeFileSync(outsideFile, "-- allium: 3\n");
    
    // Second workspace root for multi-root tests
    const secondRoot = mkdtempSync(path.join(tmpdir(), "allium-hook-second-"));
    const secondRootFile = path.join(secondRoot, "other.allium");
    writeFileSync(secondRootFile, "-- allium: 3\n");
    const secondRootInvalid = path.join(secondRoot, "broken.allium");
    writeFileSync(secondRootInvalid, "this is not valid allium\n");
    
    // Symlink inside the project pointing to a file outside it
    const symlinkFile = path.join(projectRoot, "linked.allium");
    symlinkSync(outsideFile, symlinkFile);
    
    const claudeEnv = { CLAUDE_PROJECT_ROOT: projectRoot };
    
    // --- Claude Code format: { tool_input: { file_path } } ---
    
    console.log("Claude Code — early exit:");
    
    assert(
      "missing file_path skipped",
      run({ tool_input: {} }, claudeEnv).status,
      0,
    );
    
    assert(
      "non-.allium extension skipped",
      run({ tool_input: { file_path: path.join(projectRoot, "readme.md") } }, claudeEnv).status,
      0,
    );
    
    assert(
      "non-existent .allium file skipped",
      run({ tool_input: { file_path: path.join(projectRoot, "ghost.allium") } }, claudeEnv).status,
      0,
    );
    
    console.log("\nClaude Code — path boundary:");
    
    assert(
      "file outside project root rejected",
      run({ tool_input: { file_path: outsideFile } }, claudeEnv).status,
      0,
    );
    
    assert(
      "path traversal rejected",
      run({ tool_input: { file_path: path.join(projectRoot, "..", "etc", "passwd.allium") } }, claudeEnv).status,
      0,
    );
    
    assert(
      "prefix confusion rejected",
      run({ tool_input: { file_path: projectRoot + "other/file.allium" } }, claudeEnv).status,
      0,
    );
    
    assert(
      "symlink escaping project rejected",
      run({ tool_input: { file_path: symlinkFile } }, claudeEnv).status,
      0,
    );
    
    console.log("\nClaude Code — accepted:");
    
    assert(
      "valid file at project root level",
      run({ tool_input: { file_path: validFile } }, claudeEnv).status,
      0,
    );
    
    assert(
      "valid file in nested subdirectory",
      run({ tool_input: { file_path: nestedFile } }, claudeEnv).status,
      0,
    );
    
    const invalidResult = run({ tool_input: { file_path: invalidFile } }, claudeEnv);
    assert(
      "invalid file reaches checker (exit 1)",
      invalidResult.status,
      1,
    );
    
    assert(
      "checker diagnostics forwarded to stderr",
      invalidResult.stderr.length > 0,
      true,
    );
    
    console.log("\nClaude Code — resilience:");
    
    assert(
      "invalid CLAUDE_PROJECT_ROOT exits cleanly",
      run({ tool_input: { file_path: validFile } }, { CLAUDE_PROJECT_ROOT: "/nonexistent/path" }).status,
      0,
    );
    
    // --- Cursor format: { file_path, workspace_roots } ---
    
    console.log("\nCursor — early exit:");
    
    assert(
      "missing file_path skipped",
      run({ workspace_roots: [projectRoot] }).status,
      0,
    );
    
    assert(
      "non-.allium extension skipped",
      run({ file_path: path.join(projectRoot, "readme.md"), workspace_roots: [projectRoot] }).status,
      0,
    );
    
    assert(
      "non-existent .allium file skipped",
      run({ file_path: path.join(projectRoot, "ghost.allium"), workspace_roots: [projectRoot] }).status,
      0,
    );
    
    console.log("\nCursor — path boundary:");
    
    assert(
      "file outside workspace roots rejected",
      run({ file_path: outsideFile, workspace_roots: [projectRoot] }).status,
      0,
    );
    
    assert(
      "symlink escaping workspace rejected",
      run({ file_path: symlinkFile, workspace_roots: [projectRoot] }).status,
      0,
    );
    
    console.log("\nCursor — accepted:");
    
    assert(
      "valid file at workspace root level",
      run({ file_path: validFile, workspace_roots: [projectRoot] }).status,
      0,
    );
    
    assert(
      "valid file in nested subdirectory",
      run({ file_path: nestedFile, workspace_roots: [projectRoot] }).status,
      0,
    );
    
    const cursorInvalidResult = run({ file_path: invalidFile, workspace_roots: [projectRoot] });
    assert(
      "invalid file reaches checker (exit 1)",
      cursorInvalidResult.status,
      1,
    );
    
    assert(
      "checker diagnostics forwarded to stderr",
      cursorInvalidResult.stderr.length > 0,
      true,
    );
    
    console.log("\nCursor — resilience:");
    
    assert(
      "missing workspace_roots falls back to cwd",
      run({ file_path: validFile }).status,
      0,
    );
    
    assert(
      "empty workspace_roots falls back to cwd",
      run({ file_path: validFile, workspace_roots: [] }).status,
      0,
    );
    
    // --- Windsurf format: { tool_info: { file_path }, ... } ---
    // Windsurf sends file_path inside tool_info. Working directory defaults to
    // workspace root, so no workspace_roots equivalent — cwd is the fallback.
    
    console.log("\nWindsurf — early exit:");
    
    assert(
      "missing tool_info.file_path skipped",
      run({ tool_info: {} }).sta
  • hooks/allium-lint.shGitHub
    Read the script
    #!/bin/sh
    # Wrapper for Aider's --lint-cmd. Aider passes the file path as a CLI argument.
    # Only run allium check on .allium files; exit 0 for everything else.
    case "$1" in
      *.allium) exec allium check "$1" ;;
      *) exit 0 ;;
    esac
    
  • hooks/loop-trace.mjsRunsGitHub
    Read the script
    // Records real per-subagent-call timing into the Allium loop's trace.
    //
    // Registered twice in hooks.json, matched to the subagent tool:
    //   PreToolUse  → node loop-trace.mjs pre    (stamp the call's start)
    //   PostToolUse → node loop-trace.mjs post   (write duration on return)
    //
    // Timing has to be captured outside the model — a subagent call isn't a Bash
    // call the model can wrap in `date`, and the model can't read its own latency.
    // A hook fires on the tool events, so it can. It writes one line per call to
    // .allium-loop/timings.jsonl; the loop folds those durations into its trace and
    // report (driving-the-loop §13). The model's trajectory + routing telemetry is
    // the cross-harness baseline; this hook is the deterministic timing layer where
    // hooks run (Claude Code and other editors that honour them).
    //
    // It only records while a loop is active (a .allium-loop/ dir exists), never
    // blocks a call (always exits 0), and swallows its own errors.
    
    import {
      closeSync,
      constants,
      existsSync,
      lstatSync,
      openSync,
      readFileSync,
      realpathSync,
      writeFileSync,
    } from "fs";
    import path from "path";
    
    process.on("uncaughtException", () => process.exit(0));
    
    const event = process.argv[2]; // "pre" | "post"
    if (event !== "pre" && event !== "post") process.exit(0);
    
    let data = "";
    for await (const chunk of process.stdin) data += chunk;
    
    let input;
    try {
      input = JSON.parse(data);
    } catch {
      process.exit(0);
    }
    
    // The subagent's type is the useful label ("allium:weed"). Field placement
    // varies by harness, so look in the likely spots; fall back to the tool name.
    const toolInput = input.tool_input ?? input.tool_info ?? {};
    const agent =
      toolInput.subagent_type ??
      toolInput.subagentType ??
      toolInput.agent ??
      input.tool_name ??
      input.tool ??
      "subagent";
    
    // A correlation id pairs a pre with its post. If the harness doesn't provide
    // one, fall back to FIFO, which is correct for the sequential phase calls the
    // loop makes (a documented limitation under parallel calls).
    const corrId =
      input.tool_use_id ?? input.toolUseId ?? toolInput.id ?? null;
    
    // Resolve the project root the same way the allium-check hook does.
    const payloadRoots = Array.isArray(input.workspace_roots) ? input.workspace_roots : [];
    const roots = [process.env.CLAUDE_PROJECT_ROOT, ...payloadRoots].filter(Boolean);
    if (roots.length === 0) roots.push(process.cwd());
    
    let projectRoot = null;
    for (const r of roots) {
      try {
        projectRoot = realpathSync(r);
        break;
      } catch {
        // try the next root
      }
    }
    if (!projectRoot) process.exit(0);
    
    // Only trace while a loop is running; otherwise this is an unrelated subagent.
    const loopDir = path.join(projectRoot, ".allium-loop");
    if (!existsSync(loopDir)) process.exit(0);
    try {
      const stat = lstatSync(loopDir);
      if (!stat.isDirectory() || stat.isSymbolicLink()) process.exit(0);
    } catch {
      process.exit(0);
    }
    
    const pendingPath = path.join(loopDir, ".timing-pending.json");
    const timingsPath = path.join(loopDir, "timings.jsonl");
    const now = Date.now();
    
    function readPending() {
      try {
        const stat = lstatSync(pendingPath);
        if (!stat.isFile() || stat.isSymbolicLink()) return { byId: {}, fifo: [] };
        const p = JSON.parse(readFileSync(pendingPath, "utf-8"));
        return { byId: p.byId ?? {}, fifo: Array.isArray(p.fifo) ? p.fifo : [] };
      } catch {
        return { byId: {}, fifo: [] };
      }
    }
    function writeWithoutFollowingSymlinks(filePath, data, append = false) {
      let fd;
      try {
        const noFollow = constants.O_NOFOLLOW ?? 0;
        const mode = append ? constants.O_APPEND : constants.O_TRUNC;
        fd = openSync(filePath, constants.O_WRONLY | constants.O_CREAT | mode | noFollow, 0o600);
        writeFileSync(fd, data);
        return true;
      } catch {
        return false;
      } finally {
        if (fd !== undefined) {
          try {
            closeSync(fd);
          } catch {
            // best-effort
          }
        }
      }
    }
    function writePending(p) {
      writeWithoutFollowingSymlinks(pendingPath, JSON.stringify(p));
    }
    
    if (event === "pre") {
      const p = readPending();
      if (corrId) p.byId[corrId] = { start: now, agent };
      else p.fifo.push({ start: now, agent });
      writePending(p);
      process.exit(0);
    }
    
    // event === "post"
    const p = readPending();
    let rec = null;
    if (corrId && p.byId[corrId]) {
      rec = p.byId[corrId];
      delete p.byId[corrId];
    } else if (p.fifo.length > 0) {
      rec = p.fifo.shift();
    }
    writePending(p);
    
    if (rec) {
      const line =
        JSON.stringify({
          ts: new Date(now).toISOString(),
          agent: rec.agent,
          duration_ms: now - rec.start,
        }) + "\n";
      writeWithoutFollowingSymlinks(timingsPath, line, true);
    }
    process.exit(0);
    
  • hooks/loop-trace.test.mjsGitHub
    Read the script
    import { execFileSync } from "child_process";
    import { mkdtempSync, mkdirSync, readFileSync, existsSync, rmSync, symlinkSync, writeFileSync } from "fs";
    import path from "path";
    import { tmpdir } from "os";
    
    const hook = new URL("./loop-trace.mjs", import.meta.url).pathname;
    let passed = 0;
    let failed = 0;
    
    function assert(name, cond) {
      if (cond) { console.log(`  pass: ${name}`); passed++; }
      else { console.log(`  FAIL: ${name}`); failed++; }
    }
    
    // Run the hook once (one event) with a synthetic payload and a project root.
    function runHook(event, input, projectRoot) {
      try {
        execFileSync("node", [hook, event], {
          input: JSON.stringify(input),
          encoding: "utf-8",
          stdio: ["pipe", "pipe", "pipe"],
          env: { ...process.env, CLAUDE_PROJECT_ROOT: projectRoot },
        });
      } catch {
        // the hook always exits 0; ignore
      }
    }
    
    function newProject({ withLoopDir }) {
      const dir = mkdtempSync(path.join(tmpdir(), "allium-timing-"));
      if (withLoopDir) mkdirSync(path.join(dir, ".allium-loop"));
      return dir;
    }
    function timings(dir) {
      const p = path.join(dir, ".allium-loop", "timings.jsonl");
      if (!existsSync(p)) return [];
      return readFileSync(p, "utf-8").trim().split("\n").filter(Boolean).map((l) => JSON.parse(l));
    }
    
    console.log("\n── loop-trace hook ──\n");
    
    // 1. pre + post with a correlation id → one timing line, right agent, numeric duration.
    {
      const dir = newProject({ withLoopDir: true });
      const payload = { tool_use_id: "abc", tool_name: "Task", tool_input: { subagent_type: "allium:weed" } };
      runHook("pre", payload, dir);
      runHook("post", payload, dir);
      const t = timings(dir);
      assert("records one timing for a paired pre/post", t.length === 1);
      assert("labels the timing with the subagent type", t[0]?.agent === "allium:weed");
      assert("duration is a non-negative number", typeof t[0]?.duration_ms === "number" && t[0].duration_ms >= 0);
      rmSync(dir, { recursive: true, force: true });
    }
    
    // 2. No .allium-loop dir → no-op, nothing written.
    {
      const dir = newProject({ withLoopDir: false });
      const payload = { tool_use_id: "x", tool_name: "Task", tool_input: { subagent_type: "allium:weed" } };
      runHook("pre", payload, dir);
      runHook("post", payload, dir);
      assert("does nothing when no loop is active", !existsSync(path.join(dir, ".allium-loop", "timings.jsonl")));
      rmSync(dir, { recursive: true, force: true });
    }
    
    // 3. FIFO fallback when there is no correlation id: two calls pair in order.
    {
      const dir = newProject({ withLoopDir: true });
      runHook("pre", { tool_name: "Task", tool_input: { subagent_type: "allium:distill" } }, dir);
      runHook("pre", { tool_name: "Task", tool_input: { subagent_type: "allium:propagate" } }, dir);
      runHook("post", { tool_name: "Task", tool_input: {} }, dir);
      runHook("post", { tool_name: "Task", tool_input: {} }, dir);
      const t = timings(dir);
      assert("pairs two unkeyed calls in FIFO order", t.length === 2 && t[0].agent === "allium:distill" && t[1].agent === "allium:propagate");
      rmSync(dir, { recursive: true, force: true });
    }
    
    // 4. A post with no matching pre writes nothing and does not crash.
    {
      const dir = newProject({ withLoopDir: true });
      runHook("post", { tool_use_id: "orphan", tool_name: "Task", tool_input: { subagent_type: "allium:weed" } }, dir);
      assert("ignores an unpaired post", timings(dir).length === 0);
      rmSync(dir, { recursive: true, force: true });
    }
    
    // 5. Malformed stdin is swallowed (no crash, no output).
    {
      const dir = newProject({ withLoopDir: true });
      try {
        execFileSync("node", [hook, "pre"], { input: "not json", encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"], env: { ...process.env, CLAUDE_PROJECT_ROOT: dir } });
        assert("survives malformed input", true);
      } catch {
        assert("survives malformed input", false);
      }
      rmSync(dir, { recursive: true, force: true });
    }
    
    // 6. A repository-controlled .allium-loop symlink must not redirect writes.
    {
      const dir = newProject({ withLoopDir: false });
      const outside = mkdtempSync(path.join(tmpdir(), "allium-timing-outside-"));
      symlinkSync(outside, path.join(dir, ".allium-loop"));
      const payload = { tool_use_id: "linked-dir", tool_name: "Task", tool_input: { subagent_type: "allium:weed" } };
      runHook("pre", payload, dir);
      assert("refuses a symlinked loop directory", !existsSync(path.join(outside, ".timing-pending.json")));
      rmSync(dir, { recursive: true, force: true });
      rmSync(outside, { recursive: true, force: true });
    }
    
    // 7. A symlinked pending-state file must not be read or overwritten.
    {
      const dir = newProject({ withLoopDir: true });
      const victim = path.join(dir, "victim-pending.txt");
      writeFileSync(victim, "unchanged");
      symlinkSync(victim, path.join(dir, ".allium-loop", ".timing-pending.json"));
      runHook("pre", { tool_use_id: "linked-pending", tool_name: "Task", tool_input: { subagent_type: "allium:weed" } }, dir);
      assert("refuses a symlinked pending-state file", readFileSync(victim, "utf-8") === "unchanged");
      rmSync(dir, { recursive: true, force: true });
    }
    
    // 8. A symlinked timing log must not receive appended data.
    {
      const dir = newProject({ withLoopDir: true });
      const payload = { tool_use_id: "linked-log", tool_name: "Task", tool_input: { subagent_type: "allium:weed" } };
      runHook("pre", payload, dir);
      const victim = path.join(dir, "victim-timings.txt");
      writeFileSync(victim, "unchanged");
      symlinkSync(victim, path.join(dir, ".allium-loop", "timings.jsonl"));
      runHook("post", payload, dir);
      assert("refuses a symlinked timing log", readFileSync(victim, "utf-8") === "unchanged");
      rmSync(dir, { recursive: true, force: true });
    }
    
    console.log(`\n${passed} passed, ${failed} failed`);
    process.exit(failed > 0 ? 1 : 0);
    

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.

@juxt
Ships withallium

The specification language that talks back: allium-lang.org.

5217 skills5 agents
Get the whole plugin

Stars on GitHub

Momentum

stars in total
521
Star history of juxt/allium on GitHub, a chart by star-history.com

More plugins that ship hooks.

qiushi-skill
Plugin

qiushi-skill

Helps Claude Code weigh evidence before judging, review its own finished work, and focus on one main task at a time when several compete. Each method comes from classic dialectical and practical philosophy.

11 skills+33.8k
@hughyau@hughyau
Install Plugin
taskmaster
Plugin

taskmaster

Taskmaster is a completion guard for coding agents. It addresses a common failure mode: the agent makes partial progress, writes a summary, and stops before the user goal is actually finished.

1 skill2 hooks532
@blader@blader
Install Plugin
copilot-studio
Plugin

copilot-studio

Lets you write, test and troubleshoot Microsoft Copilot Studio standard agents by editing their YAML files from your terminal or editor, instead of working only in the portal. Covers adding actions, adaptive cards, knowledge sources and generative answers.

31 skills+2482
@microsoft@microsoft
Install Plugin
codex-skill
Plugin

codex-skill

Give Claude Code a "second opinion" by letting it consult OpenAI's Codex CLI for independent verification.

1 skill1 hook219
@cathrynlavery@cathrynlavery
Install Plugin
anastasiyaw-claude-code-config
Plugin

anastasiyaw-claude-cod…

A practical configuration kit for Claude Code, Codex, and other coding agents. It contains architectural principles, enforcement hooks, skills, drop-in rules, starter templates, and dynamic-workflow commands.

59 skills+2212
@anastasiyaw@anastasiyaw
Install Plugin
uipath
Plugin

uipath

UiPath Agent Skills give AI coding agents the domain knowledge to build, run, test, and deploy UiPath automations and agents, directly from your development environment.

32 skills+2175
@uipath@uipath
Install Plugin

Questions about the hooks in allium.

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