Skip to content
Development
Hook

Hooks

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

From plugin
zenophobia
41 command3 hooks

Claude Code

> /plugin marketplace add NegativeZone/zenophobia
> /plugin install zenophobia@zenophobia

Where it lives

  • hooks/zeno-activate.jsGitHub
    Read the script
    #!/usr/bin/env node
    // zenophobia — SessionStart hook: emit ruleset at active level.
    
    const fs = require('fs');
    const path = require('path');
    const os = require('os');
    
    const claudeDir = process.env.CLAUDE_CONFIG_DIR || path.join(os.homedir(), '.claude');
    const flagPath = path.join(claudeDir, 'zenophobia', 'level');
    const LEVELS = ['lite', 'full', 'ultra'];
    
    let level = 'full';
    try {
      const raw = fs.readFileSync(flagPath, 'utf8').trim();
      if (raw === 'off') process.exit(0);
      if (LEVELS.includes(raw)) level = raw;
    } catch (e) { /* no flag file — default full */ }
    
    let rules;
    try {
      rules = fs.readFileSync(path.join(__dirname, 'zeno-rules.md'), 'utf8');
    } catch (e) {
      // Minimum viable ruleset if zeno-rules.md is missing.
      rules = 'Close the task. No trailing offers ("Want me to also...?"), no hedged dones ' +
        '("should work, you may want to verify" — verify or state "Not verified: <reason>"), ' +
        'no seeded TODOs, no follow-up bait. 100% of the task or explicit "Skipped Y: <reason>". ' +
        'Final message: what changed, evidence, then stop.';
    }
    
    process.stdout.write('ZENOPHOBIA ACTIVE — level: ' + level + '\n\n' + rules);
    
  • hooks/zeno-stop.jsGitHub
    Read the script
    #!/usr/bin/env node
    // zenophobia — Stop hook: the finish line outside the model's judgment.
    // Scans the final assistant message for loose-end species (trailing offers,
    // hedged dones, follow-up bait). On hit, blocks the stop ONCE with a reason
    // telling the model to close the loose end. Never blocks twice for the same
    // turn — a stop gate that forces "one more thing" forever would be Zeno.
    
    const fs = require('fs');
    const path = require('path');
    const os = require('os');
    
    const claudeDir = process.env.CLAUDE_CONFIG_DIR || path.join(os.homedir(), '.claude');
    const flagPath = path.join(claudeDir, 'zenophobia', 'level');
    
    function allow() { process.exit(0); }
    
    function readLevel() {
      try {
        const raw = fs.readFileSync(flagPath, 'utf8').trim();
        return ['off', 'lite', 'full', 'ultra'].includes(raw) ? raw : 'full';
      } catch (e) { return 'full'; }
    }
    
    // [species label, regex] — scanned against the tail of the final message,
    // code blocks stripped. Conservative on purpose: a false block is worse
    // than a missed offer.
    const PERFORMATIVE = [
      ['trailing-offer', /\bwant me to\b/i],
      ['trailing-offer', /\bwould you like me to\b/i],
      ['trailing-offer', /\bshould i (also|go ahead|proceed)\b/i],
      ['trailing-offer', /\bjust say the word\b/i],
      ['trailing-offer', /\blet me know if you(?:'d| would)? (?:like|want|need)\b/i],
      ['trailing-offer', /\bif you(?:'d like| like| want), i can\b/i],
      ['trailing-offer', /\bi can also (?:add|write|create|update|wire|set up|implement|expand)\b/i],
      ['trailing-offer', /\bhappy to (?:add|write|do|expand|dig|implement)\b/i],
      ['follow-up-bait', /\banything else[^.?!]{0,30}\?/i],
      ['follow-up-bait', /\bwhich of (?:these|those) would you\b/i],
    ];
    const HEDGED = [
      ['hedged-done', /\bshould (?:now )?work\b/i],
      ['hedged-done', /\bshould be (?:fixed|working|good to go)\b/i],
      ['hedged-done', /\byou may want to (?:verify|double[- ]check|test)\b/i],
      ['hedged-done', /\bprobably (?:fixed|works)\b/i],
    ];
    const ULTRA_EXTRA = [
      ['hedged-done', /\blikely (?:fixed|works)\b/i],
      ['next-steps', /\bnext steps?\b/i],
    ];
    
    function lastAssistantText(transcriptPath) {
      let text = null;
      const lines = fs.readFileSync(transcriptPath, 'utf8').split('\n');
      for (const line of lines) {
        if (!line.trim()) continue;
        let o;
        try { o = JSON.parse(line); } catch (e) { continue; }
        if (o.type !== 'assistant' || !o.message || !Array.isArray(o.message.content)) continue;
        const parts = o.message.content.filter(c => c.type === 'text' && c.text);
        if (parts.length) text = parts.map(c => c.text).join('\n');
      }
      return text;
    }
    
    let input = '';
    process.stdin.on('data', c => { input += c; });
    process.stdin.on('error', allow);
    process.stdin.on('end', () => {
      try {
        const data = JSON.parse(input);
    
        // Loop guard 1: already continuing because of a stop hook.
        if (data.stop_hook_active) allow();
    
        // Loop guard 2: marker file — only for harnesses whose payload lacks
        // stop_hook_active entirely. When the field is present (Claude Code always
        // sends it), guard 1 is authoritative — and since real payloads carry no
        // prompt_id, the marker key would degrade to session_id and kill the gate
        // for the rest of the session after one catch.
        const useMarker = !('stop_hook_active' in data);
        // encodeURIComponent: a session_id with a path separator must not turn
        // the marker into an unwritable path (which would re-block forever).
        const marker = path.join(os.tmpdir(),
          'zenophobia-' + encodeURIComponent(String(data.session_id || 'nosession')));
        const turnKey = String(data.prompt_id || data.session_id || 'unknown');
        if (useMarker) {
          try {
            if (fs.readFileSync(marker, 'utf8') === turnKey) allow();
          } catch (e) { /* no marker yet */ }
        }
    
        const level = readLevel();
        if (level === 'off' || level === 'lite') allow();
    
        // Claude Code sends the final text directly (verified in v2.1.217's Stop
        // payload); transcript parse is the fallback for harnesses that don't.
        let text = typeof data.last_assistant_message === 'string' && data.last_assistant_message
          ? data.last_assistant_message : null;
        if (!text && data.transcript_path) text = lastAssistantText(data.transcript_path);
        if (!text) allow();
    
        // Loose ends live at the end of the message ("you'll know it by the way
        // it arrives after the answer was already complete"). Scan the tail only,
        // code blocks stripped — quoted or generated code is not the model talking.
        // (?:```|$) also strips a trailing unclosed fence — a message that ends
        // mid-code-block must not leak quoted text into the scan.
        const tail = text.replace(/```[\s\S]*?(?:```|$)/g, '').slice(-700);
    
        const checks = level === 'ultra'
          ? PERFORMATIVE.concat(HEDGED, ULTRA_EXTRA)
          : PERFORMATIVE.concat(HEDGED);
    
        for (const [species, re] of checks) {
          const m = re.exec(tail);
          if (!m) continue;
    
          // Only block if the marker persists — an unpersistable marker would
          // re-block this turn forever in a harness without stop_hook_active.
          // Fail open.
          if (useMarker) {
            try { fs.writeFileSync(marker, turnKey); } catch (e) { allow(); }
          }
          try {
            const logDir = path.join(claudeDir, 'zenophobia');
            fs.mkdirSync(logDir, { recursive: true });
            fs.appendFileSync(path.join(logDir, 'kills.jsonl'),
              JSON.stringify({ ts: new Date().toISOString(), species, match: m[0], level }) + '\n');
          } catch (e) { /* stats are optional, blocking is not */ }
    
          process.stdout.write(JSON.stringify({
            decision: 'block',
            reason: 'ZENOPHOBIA stop gate — loose end in your final message (' + species +
              ': "' + m[0] + '"). Close it: if the offered work is in scope, do it now; ' +
              'if out of scope, delete the offer. Replace any hedge with a verified result ' +
              '(command + exit code) or one flat "Not verified: <concrete reaso
  • hooks/zeno-tracker.jsGitHub
    Read the script
    #!/usr/bin/env node
    // zenophobia — UserPromptSubmit hook: track /zenophobia level switches,
    // re-inject a one-line reminder every turn while active.
    
    const fs = require('fs');
    const path = require('path');
    const os = require('os');
    
    const claudeDir = process.env.CLAUDE_CONFIG_DIR || path.join(os.homedir(), '.claude');
    const flagPath = path.join(claudeDir, 'zenophobia', 'level');
    const LEVELS = ['lite', 'full', 'ultra'];
    
    function writeLevel(v) {
      try {
        fs.mkdirSync(path.dirname(flagPath), { recursive: true });
        fs.writeFileSync(flagPath, v);
      } catch (e) {
        // Never fail the prompt, but a silent no-op on "/zenophobia off" is worse
        // than a stderr line.
        process.stderr.write('zenophobia: could not write level: ' + e.message + '\n');
      }
    }
    
    function readLevel() {
      try {
        const raw = fs.readFileSync(flagPath, 'utf8').trim();
        return raw === 'off' || LEVELS.includes(raw) ? raw : 'full';
      } catch (e) { return 'full'; }
    }
    
    let input = '';
    process.stdin.on('data', c => { input += c; });
    process.stdin.on('error', () => process.exit(0)); // hooks must always exit 0
    process.stdin.on('end', () => {
      try {
        const data = JSON.parse(input);
        const prompt = (data.prompt || '').trim().toLowerCase().replace(/\s+/g, ' ');
    
        // Deactivation first, so "turn zenophobia off" never re-arms it.
        if (/\b(stop|disable|deactivate|quit|exit)\s+(the\s+)?zenophobia\b/.test(prompt) ||
            /\bzenophobia\s+(off|stop|disabled?)\b/.test(prompt) ||
            /^(please\s+)?(go\s+|back\s+to\s+|switch\s+(back\s+)?to\s+)?normal\s+mode\b/.test(prompt)) {
          writeLevel('off');
          return;
        }
    
        // /zenophobia [level] — plain and marketplace-namespaced forms.
        const m = /^\/zenophobia(?::zenophobia)?(?=\s|$)(?:\s+(\S+))?/.exec(prompt);
        if (m) {
          const arg = m[1] || 'full';
          if (LEVELS.includes(arg) || arg === 'off') {
            writeLevel(arg);
          }
          // Unknown arg: flag untouched.
        }
    
        const level = readLevel();
        if (level !== 'off') {
          process.stdout.write(JSON.stringify({
            hookSpecificOutput: {
              hookEventName: 'UserPromptSubmit',
              additionalContext: 'ZENOPHOBIA ACTIVE (' + level + '). Close the task. ' +
                'No trailing offers, no follow-up bait, no hedged dones, no seeded TODOs. ' +
                'Done = evidence or flat "Not verified: <reason>". Then stop.'
            }
          }));
        }
      } catch (e) { /* silent fail — never block the prompt */ }
    });
    

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.

@negativezone
Ships withzenophobia

Fear of Zeno. Makes your coding agent finish. Ask an agent for a one-line fix and you get the fix plus a question: want me to also add tests? Say yes and the tests arrive with an offer to wire up CI. The task goes 90% done, then 95, then 97.5.

Stars on GitHub

Momentum

stars in total
4
Star history of NegativeZone/zenophobia 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 hook318k
@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 hooks191k
@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+3117k
@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+312k
@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 zenophobia.

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