Skip to content
Writing
Hook

Hooks

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

From plugin
genshijin
3327 skills3 agents6 commands10 hooks
Install
> /plugin marketplace add InterfaceX-co-jp/genshijin
> /plugin install genshijin@genshijin

Ships with genshijin. Installing the plugin gets these hooks.

Where it lives

  • hooks/genshijin-activate.jsGitHub
    Read the script
    #!/usr/bin/env node
    // genshijin — Claude Code SessionStart アクティベーションフック
    //
    // セッション開始毎に実行:
    //   1. フラグファイル $CLAUDE_CONFIG_DIR/.genshijin-active 書込(statusline が読む)
    //   2. genshijin ルールセットを hidden SessionStart context として注入
    //   3. statusline 未設定を検知しセットアップを促す
    
    const fs = require('fs');
    const path = require('path');
    const os = require('os');
    const {
      getDefaultMode,
      safeWriteFlag,
      readFlag,
      recordModeChange,
      VALID_MODES,
      MODE_TO_LABEL,
    } = require('./genshijin-config');
    
    const claudeDir = process.env.CLAUDE_CONFIG_DIR || path.join(os.homedir(), '.claude');
    const flagPath = path.join(claudeDir, '.genshijin-active');
    const settingsPath = path.join(claudeDir, 'settings.json');
    
    // SessionStart は resume / clear / compaction 時にも再実行される。
    // 真の startup 以外では、ユーザーがセッション中に切替えたモードを保持。
    let source = 'startup';
    let sessionId = null;
    try {
      if (!process.stdin.isTTY) {
        const raw = fs.readFileSync(0, 'utf8');
        if (raw) {
          const data = JSON.parse(raw);
          if (data && typeof data.source === 'string') source = data.source;
          if (data && typeof data.session_id === 'string') sessionId = data.session_id;
        }
      }
    } catch (e) {
      // stdinなし/不正時は startup 扱い
    }
    
    let mode = getDefaultMode();
    if (source !== 'startup') {
      const existing = readFlag(flagPath);
      if (existing && VALID_MODES.includes(existing)) mode = existing;
    }
    
    // "off" モード — アクティベート一切せず、フラグ削除してルール注入なし
    if (mode === 'off') {
      const previous = readFlag(flagPath);
      if (safeWriteFlag(flagPath, 'off')) {
        recordModeChange(claudeDir, null, sessionId, previous);
      }
      process.stdout.write('OK');
      process.exit(0);
    }
    
    // 1. フラグファイル書込(symlink-safe)
    const previous = readFlag(flagPath);
    if (safeWriteFlag(flagPath, mode)) {
      recordModeChange(claudeDir, mode, sessionId, previous);
    }
    
    // 独立スキルモード(サブスキル側で挙動定義)
    const INDEPENDENT_MODES = new Set(['commit', 'review', 'compress', 'help']);
    
    if (INDEPENDENT_MODES.has(mode)) {
      process.stdout.write('原始人モード有効 — レベル: ' + mode + '。挙動は /genshijin-' + mode + ' スキル側で定義。');
      process.exit(0);
    }
    
    // SKILL.md 表記ラベル(日本語)に変換
    const modeLabel = MODE_TO_LABEL[mode] || mode;
    
    // SKILL.md を実行時読込 — genshijin 挙動の唯一の真実源。
    // プラグイン導入: __dirname = <plugin_root>/hooks/, SKILL.md は <plugin_root>/skills/genshijin/SKILL.md
    // standalone 導入: __dirname = $CLAUDE_CONFIG_DIR/hooks/, SKILL.md 不在 → ハードコードフォールバック。
    let skillContent = '';
    try {
      skillContent = fs.readFileSync(
        path.join(__dirname, '..', 'skills', 'genshijin', 'SKILL.md'), 'utf8'
      );
    } catch (e) { /* standalone 導入 — 下のフォールバックを使用 */ }
    
    let output;
    
    if (skillContent) {
      // YAML frontmatter 除去
      const body = skillContent.replace(/^---[\s\S]*?---\s*/, '');
    
      // intensity テーブル行を filter: ヘッダ/区切りは残し、アクティブレベル行のみ残す
      const filtered = body.split('\n').reduce((acc, line) => {
        // intensity テーブル行: | **丁寧** | ...
        const tableRowMatch = line.match(/^\|\s*\*\*(\S+?)\*\*\s*\|/);
        if (tableRowMatch) {
          if (tableRowMatch[1] === modeLabel) {
            acc.push(line);
          }
          return acc;
        }
    
        // 例の行: "- 丁寧:" 形式 — アクティブレベル行のみ残す
        const exampleMatch = line.match(/^-\s*(丁寧|通常|極限):\s/);
        if (exampleMatch) {
          if (exampleMatch[1] === modeLabel) {
            acc.push(line);
          }
          return acc;
        }
    
        acc.push(line);
        return acc;
      }, []);
    
      output = '原始人モード有効 — レベル: ' + modeLabel + '\n\n' + filtered.join('\n');
    } else {
      // SKILL.md 不在時のフォールバック(standalone フック導入 + skills ディレクトリなし)
      output =
        '原始人モード有効 — レベル: ' + modeLabel + '\n\n' +
        '原始人のように簡潔に返答せよ。技術的中身はすべて残す。無駄だけ消す。\n\n' +
        '## 永続性\n\n' +
        '毎レスポンス有効。多ターン経過後も敬語回帰・フィラー漂流 禁止。不明時も維持。\n' +
        '解除条件のみ: 「原始人やめて」「通常モード」。\n\n' +
        '現レベル: **' + modeLabel + '**。切替: `/genshijin 丁寧|通常|極限`\n\n' +
        '## ルール\n\n' +
        '削除: 敬語・クッション言葉・前置き・ぼかし・冗長助詞。体言止め/用言止め可。' +
        '技術用語はそのまま。コードブロック/エラーメッセージ原文維持。\n\n' +
        'パターン: `[対象] [状態/動作] [理由]。[次の手順]。`\n\n' +
        '## 境界\n\n' +
        'コード/コミット/PR: 通常記述。「原始人やめて」「通常モード」で解除。' +
        'レベルは変更またはセッション終了まで維持。';
    }
    
    // 3. statusline 未設定検知 → セットアップを Claude に促す
    try {
      let hasStatusline = false;
      if (fs.existsSync(settingsPath)) {
        const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
        if (settings.statusLine) {
          hasStatusline = true;
        }
      }
    
      if (!hasStatusline) {
        const isWindows = process.platform === 'win32';
        const scriptName = isWindows ? 'genshijin-statusline.ps1' : 'genshijin-statusline.sh';
        const scriptPath = path.join(__dirname, scriptName);
        const command = isWindows
          ? `powershell -ExecutionPolicy Bypass -File "${scriptPath}"`
          : `bash "${scriptPath}"`;
        const statusLineSnippet =
          '"statusLine": { "type": "command", "command": ' + JSON.stringify(command) + ' }';
        output += "\n\n" +
          "STATUSLINE 未設定: genshijin プラグインは現モード表示用 statusline バッジ " +
          "(例: [原始人], [原始人:極限])を同梱。未設定状態。" +
          "有効化するには " + path.join(claudeDir, 'settings.json') + " に次を追加: " +
          statusLineSnippet + " " +
          "初回対話時にユーザーにセットアップを proactively 提案すること。";
      }
    } catch (e) {
      // silent fail — statusline 検出失敗でセッション開始を止めない
    }
    
    process.stdout.write(output);
    
  • hooks/genshijin-config.jsGitHub
    Read the script
    #!/usr/bin/env node
    // genshijin — 共有設定リゾルバ
    //
    // defaultMode の解決順:
    //   1. GENSHIJIN_DEFAULT_MODE 環境変数
    //   2. 設定ファイル defaultMode フィールド:
    //      - $XDG_CONFIG_HOME/genshijin/config.json(設定されていれば任意プラットフォーム)
    //      - ~/.config/genshijin/config.json(macOS / Linux フォールバック)
    //      - %APPDATA%\genshijin\config.json(Windows フォールバック)
    //   3. 'normal'
    
    const fs = require('fs');
    const path = require('path');
    const os = require('os');
    
    // 内部モード識別子は ASCII で統一。UI/SKILL.md 表記は日本語にマッピング。
    const VALID_MODES = [
      'off', 'polite', 'normal', 'extreme',
      'commit', 'review', 'compress', 'help'
    ];
    
    // ASCII モード → SKILL.md intensity 表記(日本語ラベル)
    const MODE_TO_LABEL = {
      polite: '丁寧',
      normal: '通常',
      extreme: '極限'
    };
    
    function getConfigDir() {
      if (process.env.XDG_CONFIG_HOME) {
        return path.join(process.env.XDG_CONFIG_HOME, 'genshijin');
      }
      if (process.platform === 'win32') {
        return path.join(
          process.env.APPDATA || path.join(os.homedir(), 'AppData', 'Roaming'),
          'genshijin'
        );
      }
      return path.join(os.homedir(), '.config', 'genshijin');
    }
    
    function getConfigPath() {
      return path.join(getConfigDir(), 'config.json');
    }
    
    function getDefaultMode() {
      const envMode = process.env.GENSHIJIN_DEFAULT_MODE;
      if (envMode && VALID_MODES.includes(envMode.toLowerCase())) {
        return envMode.toLowerCase();
      }
    
      try {
        const configPath = getConfigPath();
        const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
        if (config.defaultMode && VALID_MODES.includes(config.defaultMode.toLowerCase())) {
          return config.defaultMode.toLowerCase();
        }
      } catch (e) {
        // 設定ファイル不在 or 不正 → フォールスルー
      }
    
      return 'normal';
    }
    
    // Symlink-safe フラグファイル書込。immediate parent ディレクトリと対象ファイル
    // 両方で symlink を拒否し、O_NOFOLLOW を使い、temp + rename で 0o600 アトミック書込。
    // 予測可能なフラグパス(~/.claude/.genshijin-active)を symlink で差し替えて
    // 他のファイルを破壊する攻撃を塞ぐ。
    //
    // 注: ~/.claude 自体が symlink の環境(Nix / dotfiles管理 / Docker bind-mount)
    // が存在するため、parent dir resolved path のみチェック。フルチェイン検証は誤拒否多。
    function safeWriteFlag(flagPath, content) {
      try {
        const flagDir = path.dirname(flagPath);
        fs.mkdirSync(flagDir, { recursive: true });
    
        // flagPath 自体が symlink なら拒否 (filename自体の差替防止)
        try {
          if (fs.lstatSync(flagPath).isSymbolicLink()) return;
        } catch (e) {
          if (e.code !== 'ENOENT') return;
        }
    
        let tempPath;
        try {
          tempPath = path.join(
            flagDir,
            `.genshijin-active.${process.pid}.${Date.now()}.${Math.random().toString(16).slice(2)}`
          );
          const O_NOFOLLOW = typeof fs.constants.O_NOFOLLOW === 'number' ? fs.constants.O_NOFOLLOW : 0;
          const flags = fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_EXCL | O_NOFOLLOW;
          let fd;
          try {
            fd = fs.openSync(tempPath, flags, 0o600);
            fs.writeSync(fd, String(content));
            try { fs.fchmodSync(fd, 0o600); } catch (e) { /* Windows ベストエフォート */ }
          } finally {
            if (fd !== undefined) fs.closeSync(fd);
          }
    
          let renamed = false;
          let windowsPrevious = null;
          let windowsRemovedOriginal = false;
          for (let attempt = 0; attempt < 3; attempt++) {
            try {
              fs.renameSync(tempPath, flagPath);
              renamed = true;
              break;
            } catch (e) {
              const transient = ['EPERM', 'EBUSY', 'EACCES', 'EEXIST'].includes(e.code);
              if (!transient) throw e;
              if (process.platform === 'win32') {
                // 初回削除後に宛先が再出現した場合は別writerの成功。上書きしない。
                if (windowsRemovedOriginal) break;
                // Windows rename は既存宛先を置換不可。削除後renameなら部分書込を避けられる。
                try {
                  const current = fs.lstatSync(flagPath);
                  if (current.isSymbolicLink() || !current.isFile()) return;
                  if (windowsPrevious === null) {
                    windowsPrevious = fs.readFileSync(flagPath);
                  }
                  fs.unlinkSync(flagPath);
                  windowsRemovedOriginal = true;
                } catch (unlinkError) {
                  if (unlinkError.code !== 'ENOENT') continue;
                }
              }
            }
          }
          if (!renamed) {
            if (windowsPrevious !== null) {
              try {
                fs.writeFileSync(flagPath, windowsPrevious, { flag: 'wx', mode: 0o600 });
              } catch (e) { /* 別writer成功済み or 復元不能 */ }
            }
            return;
          }
          return true;
        } finally {
          if (tempPath) {
            try { fs.unlinkSync(tempPath); } catch (e) { /* rename済み or 未作成 */ }
          }
        }
      } catch (e) {
        // silent fail — フラグはベストエフォート
        return false;
      }
    }
    
    // symlink 拒否・サイズ上限・ホワイトリスト検証付きフラグ読込。
    // 攻撃者が ~/.ssh/id_rsa 等への symlink で差替えた場合でも、
    // 不正値として null を返しコンテキストに混入させない。
    const MAX_FLAG_BYTES = 64;
    
    function readFlag(flagPath) {
      try {
        let st;
        try {
          st = fs.lstatSync(flagPath);
        } catch (e) {
          return null;
        }
        if (st.isSymbolicLink() || !st.isFile()) return null;
        if (st.size > MAX_FLAG_BYTES) return null;
    
        const O_NOFOLLOW = typeof fs.constants.O_NOFOLLOW === 'number' ? fs.constants.O_NOFOLLOW : 0;
        const flags = fs.constants.O_RDONLY | O_NOFOLLOW;
        let fd;
        let out;
        try {
          fd = fs.openSync(flagPath, flags);
          const buf = Buffer.alloc(MAX_FLAG_BYTES);
          const n = fs.readSync(fd, buf, 0, MAX_FLAG_BYTES, 0);
          out = buf.slice(0, n).toString('utf8');
        } finally {
          if (fd !== undefined) fs.closeSync(fd);
        }
    
        const raw = out.trim().toLowerCase();
        if (!VALID_MODES.includes(raw)) return null;
        return raw;
      } catch (e) {
        return null;
      }
    }
    
    // 履歴ログ append (lifetime stats 用)。symlink-safe + JSONL line append。
    // readFlag/safeWriteFlag と違いサイズ上限なし。1行=1JSONエントリ前提。
    function appendFlag(flagPath, line) {
      try {
        const flagDir = path.dirname(flagPath);
        fs.mkdirSync(flagDir, { recursive: true });
    
        try {
          if (fs.lstatSync(flagPath).isSymbolicLink()) return;
        } catch (e) {
          if (e.code !== 'ENOENT') return;
        }
    
        const O_NOFOLLOW = typeof fs.constants.O_NOFOLLOW === 'number' ? 
  • hooks/genshijin-mode-tracker.jsGitHub
    Read the script
    #!/usr/bin/env node
    // genshijin — UserPromptSubmit フック。どの genshijin モードが有効か追跡。
    // ユーザー入力から /genshijin 系コマンド or 自然言語トリガーを検出し、
    // フラグファイルにモード書込。毎ターン補強リマインダも注入。
    
    const fs = require('fs');
    const path = require('path');
    const os = require('os');
    const { execFileSync } = require('child_process');
    const {
      getDefaultMode,
      safeWriteFlag,
      readFlag,
      recordModeChange,
      VALID_MODES,
    } = require('./genshijin-config');
    
    // 独立スキルモード — 自身のスラッシュコマンド (/genshijin-commit 等) で起動。
    // /genshijin <arg> 経由で選択不可。
    const INDEPENDENT_MODES = new Set(['commit', 'review', 'compress', 'help']);
    
    // /genshijin <arg> で選択可能なベースモード(独立スキル除外)
    const BASE_MODES = new Set(['polite', 'normal', 'extreme']);
    
    const claudeDir = process.env.CLAUDE_CONFIG_DIR || path.join(os.homedir(), '.claude');
    const flagPath = path.join(claudeDir, '.genshijin-active');
    const prevPath = path.join(claudeDir, '.genshijin-active.prev');
    
    function setActiveMode(mode, sessionId) {
      const previous = readFlag(flagPath);
      const stored = mode || 'off';
      if (safeWriteFlag(flagPath, stored)) {
        recordModeChange(claudeDir, mode || null, sessionId, previous);
        return true;
      }
      return false;
    }
    
    let input = '';
    process.stdin.on('data', chunk => { input += chunk; });
    process.stdin.on('error', () => process.exit(0));
    process.stdin.on('end', () => {
      try {
        const data = JSON.parse(input);
        let prompt = (data.prompt || '').trim();
        let normalized = prompt.toLowerCase().replace(/\s+/g, ' ');
    
        if (/<scheduled-task\b/.test(normalized)) return;
    
        // Claude Code は slash command をXML envelopeとして渡すため、literal形式へ復元。
        // 他コマンドの引数による自然言語トリガー誤発火は抑止。
        let skipNaturalLanguage = false;
        const envelopeName = /<command-name>\s*([^<\s]+)\s*<\/command-name>/.exec(normalized);
        if (envelopeName) {
          if (envelopeName[1].startsWith('/genshijin')) {
            const envelopeArgs = /<command-args>\s*([^<]*?)\s*<\/command-args>/.exec(normalized);
            const args = envelopeArgs ? envelopeArgs[1].trim() : '';
            prompt = args ? `${envelopeName[1]} ${args}` : envelopeName[1];
            normalized = prompt.toLowerCase().replace(/\s+/g, ' ');
          } else {
            skipNaturalLanguage = true;
          }
        }
        const lower = normalized;
    
        // /genshijin-stats — フック側でブロック実行し結果を reason として注入。
        // モデルは何もせずユーザーに数値を即時表示する。
        const statsMatch = /^\/genshijin(?::genshijin)?-stats(?:\s+(.*))?$/i.exec(prompt);
        if (statsMatch) {
          const tailArgs = (statsMatch[1] || '').trim().split(/\s+/).filter(Boolean);
          let block;
          try {
            const statsPath = path.join(__dirname, 'genshijin-stats.js');
            const argv = [statsPath];
            if (data.transcript_path) argv.push('--session-file', data.transcript_path);
            if (tailArgs.includes('--share')) argv.push('--share');
            if (tailArgs.includes('--all')) argv.push('--all');
            const sinceIdx = tailArgs.indexOf('--since');
            if (sinceIdx !== -1 && tailArgs[sinceIdx + 1]) {
              argv.push('--since', tailArgs[sinceIdx + 1]);
            }
            block = execFileSync(process.execPath, argv, {
              encoding: 'utf8',
              timeout: 5000,
            }).trim();
          } catch (e) {
            block = 'genshijin-stats: 起動失敗。手動実行: node hooks/genshijin-stats.js';
          }
          process.stdout.write(JSON.stringify({
            hookSpecificOutput: {
              hookEventName: 'UserPromptSubmit',
              additionalContext: '次のstatsブロックをコードフェンス内へ原文どおり表示。他の文章は禁止。\n\n' + block,
            },
          }));
          return;
        }
    
        // 自然言語アクティベーション
        const isQuestionJa = /(とは|って何|何ですか|何なの|について(?:説明|教えて)|どういう)/.test(prompt);
        const isQuestionEn = /^(?:what|what's|how|why|when|where|who|does|do|did|is|are|can|could|would|should|explain|tell me)\b/i.test(lower);
        const activateJa = !isQuestionJa && /(原始人|げんしじん)/.test(prompt) &&
          /(モード|起動|有効|オン|で|にして|化)/.test(prompt) &&
          !/(やめて|解除|停止|オフ|無効)/.test(prompt);
        const activateEn = !isQuestionEn && (
          /\b(activate|enable|turn on|start|talk like)\b.*\bgenshijin\b/i.test(prompt) ||
          /\bgenshijin\b.*\b(mode|activate|enable|turn on|start)\b/i.test(prompt)
        );
    
        if (!skipNaturalLanguage && (activateJa || activateEn) &&
            !/\b(stop|disable|turn off|deactivate)\b/i.test(lower)) {
          const m = getDefaultMode();
          if (m !== 'off') {
            setActiveMode(m, data.session_id);
          }
        }
    
        let setIndependentThisTurn = false;
    
        // /genshijin 系コマンド検出
        if (lower.startsWith('/genshijin')) {
          const parts = prompt.split(/\s+/);
          const cmd = parts[0].toLowerCase();
          const arg = (parts[1] || '').trim();
    
          let mode = null;
    
          if (cmd === '/genshijin-commit' || cmd === '/genshijin:genshijin-commit') {
            mode = 'commit';
          } else if (cmd === '/genshijin-review' || cmd === '/genshijin:genshijin-review') {
            mode = 'review';
          } else if (cmd === '/genshijin-compress' || cmd === '/genshijin:genshijin-compress') {
            mode = 'compress';
          } else if (cmd === '/genshijin-help' || cmd === '/genshijin:genshijin-help') {
            mode = 'help';
          } else if (cmd === '/genshijin' || cmd === '/genshijin:genshijin') {
            // 引数なし → デフォルトモード起動
            if (!arg) {
              mode = getDefaultMode();
            } else if (arg === 'off' || arg === 'stop' || arg === 'disable') {
              mode = 'off';
            } else if (arg === '丁寧' || arg.toLowerCase() === 'polite' || arg.toLowerCase() === 'teinei') {
              mode = 'polite';
            } else if (arg === '通常' || arg.toLowerCase() === 'normal' || arg.toLowerCase() === 'futsuu') {
              mode = 'normal';
            } else if (arg === '極限' || arg.toLowerCase() === 'extreme' || arg.toLowerCase() === 'kyokugen') {
              mode = 'extreme';
            }
            // 不正引数 → mode は null のまま、フラグ未変更(silent overwrite 防止)
          }
    
          // ホワイトリスト最終検証 — INDEPENDENT_MODES + BASE_MODES のみ書込許可
          if (mode === 'off') {
            setActiveMode(null, data.session_id);
            try { fs.unlinkSync(prevPath); } catch (e) {}
          } else if
  • hooks/genshijin-stats.jsGitHub
    Read the script
    #!/usr/bin/env node
    // genshijin-stats — Claude Code セッションログを読み、リアルトークン使用量と
    // ベンチマークから推定削減トークン/USDを表示。
    //
    // 直接実行:    node hooks/genshijin-stats.js
    // Claude内:    /genshijin-stats が UserPromptSubmit hook 経由で起動。
    // hook integration では --session-file <transcript_path> を渡すため、
    // アクティブセッション以外の最新JSONLを誤読しない。
    
    const fs = require('fs');
    const path = require('path');
    const os = require('os');
    const {
      readFlag,
      appendFlag,
      readHistory,
      safeWriteFlag,
      VALID_MODES,
      MODE_LOG_BASENAME,
    } = require('./genshijin-config');
    
    // benchmarks/results/*.json の平均削減率。caveman 本家は 'full' のみ計測済。
    // genshijin は 通常モード = 0.65 と仮置き(benchmarks/run.py 結果反映時に更新)。
    const COMPRESSION = { 'normal': 0.65 };
    
    const DEFAULT_RULE_OVERHEAD_TOKENS_PER_TURN = 1250;
    
    function ruleOverheadPerTurn() {
      const raw = process.env.GENSHIJIN_RULE_OVERHEAD_TOKENS;
      if (raw === undefined) return DEFAULT_RULE_OVERHEAD_TOKENS_PER_TURN;
      const value = Number(raw);
      return Number.isInteger(value) && value > 0
        ? value
        : DEFAULT_RULE_OVERHEAD_TOKENS_PER_TURN;
    }
    
    function deriveNet({ estSavedTokens, turns }) {
      const overheadTokens = Math.max(0, turns || 0) * ruleOverheadPerTurn();
      return {
        overheadTokens,
        netTokens: (estSavedTokens || 0) - overheadTokens,
      };
    }
    
    // Anthropic 公開 output token 価格 USD per million。モデルID prefix で照合 →
    // claude-sonnet-4-20250514, claude-sonnet-4-7 等のポイントリリース横断対応。
    // 価格変更時は https://www.anthropic.com/pricing から更新。
    const MODEL_OUTPUT_PRICE_PER_M = [
      ['claude-opus-4-0',    75.00],
      ['claude-opus-4-1',    75.00],
      ['claude-opus-4-2025', 75.00],
      ['claude-opus-4',      25.00],
      ['claude-sonnet-4',   15.00],
      ['claude-haiku-4',      5.00],
      ['claude-3-5-sonnet', 15.00],
      ['claude-3-5-haiku',   4.00],
      ['claude-3-opus',     75.00],
    ];
    
    function priceForModel(model) {
      if (!model) return null;
      for (const [prefix, price] of MODEL_OUTPUT_PRICE_PER_M) {
        if (model.startsWith(prefix)) return price;
      }
      return null;
    }
    
    function formatUsd(amount) {
      if (amount >= 1) return `$${amount.toFixed(2)}`;
      if (amount >= 0.01) return `$${amount.toFixed(3)}`;
      return `$${amount.toFixed(4)}`;
    }
    
    function findRecentSession(claudeDir) {
      const projectsDir = path.join(claudeDir, 'projects');
      let entries;
      try { entries = fs.readdirSync(projectsDir, { withFileTypes: true }); }
      catch { return null; }
    
      let best = null;
      const stack = entries.map(e => path.join(projectsDir, e.name));
      while (stack.length) {
        const p = stack.pop();
        let st;
        try { st = fs.statSync(p); } catch { continue; }
        if (st.isDirectory()) {
          try {
            for (const child of fs.readdirSync(p)) stack.push(path.join(p, child));
          } catch {}
        } else if (p.endsWith('.jsonl') && (!best || st.mtimeMs > best.mtime)) {
          best = { file: p, mtime: st.mtimeMs };
        }
      }
      return best ? best.file : null;
    }
    
    function parseSession(filePath) {
      let raw;
      try { raw = fs.readFileSync(filePath, 'utf8'); }
      catch {
        return {
          outputTokens: 0,
          cacheReadTokens: 0,
          turns: 0,
          model: null,
          messages: [],
        };
      }
    
      let outputTokens = 0;
      let cacheReadTokens = 0;
      let turns = 0;
      let model = null;
      const messages = [];
      for (const line of raw.split('\n')) {
        if (!line.trim()) continue;
        let entry;
        try { entry = JSON.parse(line); } catch { continue; }
        if (entry.type !== 'assistant' || !entry.message) continue;
        const usage = entry.message.usage;
        if (!usage) continue;
        outputTokens    += usage.output_tokens           || 0;
        cacheReadTokens += usage.cache_read_input_tokens || 0;
        turns++;
        if (!model && entry.message.model) model = entry.message.model;
        const ts = entry.timestamp ? Date.parse(entry.timestamp) : NaN;
        messages.push({
          ts: Number.isFinite(ts) ? ts : null,
          outputTokens: usage.output_tokens || 0,
        });
      }
      return { outputTokens, cacheReadTokens, turns, model, messages };
    }
    
    // genshijin-compress が残す *.original.md / *.md ペアを検出。
    // *.original.md バックアップが存在 → 兄弟 *.md は圧縮済メモリファイル。
    // セッション開始毎に圧縮版を読込 → サイズ差 = セッション毎 input側削減 (passive)。
    function findCompressedPairs(dirs) {
      const pairs = [];
      for (const dir of dirs) {
        let entries;
        try { entries = fs.readdirSync(dir, { withFileTypes: true }); }
        catch { continue; }
        for (const entry of entries) {
          if (!entry.isFile() || !entry.name.endsWith('.original.md')) continue;
          const base = entry.name.slice(0, -'.original.md'.length);
          const originalPath = path.join(dir, entry.name);
          const compressedPath = path.join(dir, `${base}.md`);
          let oSize, cSize;
          try {
            oSize = fs.statSync(originalPath).size;
            cSize = fs.statSync(compressedPath).size;
          } catch { continue; }
          if (oSize <= cSize) continue;
          pairs.push({ name: base, dir, originalSize: oSize, compressedSize: cSize });
        }
      }
      return pairs;
    }
    
    function summarizeCompressed(pairs) {
      if (!pairs || pairs.length === 0) return null;
      const totalOriginal = pairs.reduce((s, p) => s + p.originalSize, 0);
      const totalCompressed = pairs.reduce((s, p) => s + p.compressedSize, 0);
      const bytesSaved = totalOriginal - totalCompressed;
      // 日本語散文は 1.5〜2 char/token。ASCII含む混在で平均 ~3 byte/token と仮置き。
      // 概算ラベル付き。
      const tokensSaved = Math.round(bytesSaved / 3);
      return { count: pairs.length, bytesSaved, tokensSaved };
    }
    
    function readModeLog(logPath, sessionId) {
      const rows = [];
      for (const line of readHistory(logPath)) {
        let entry;
        try { entry = JSON.parse(line); } catch { continue; }
        if (!entry || typeof entry !== 'object' || !Number.isFinite(entry.ts)) continue;
        if (sessionId && entry.session_id !== sessionId) continue;
        const normalize = value => {
          if (value == null) return null;
          const mode = String(value);
          return VALID_MODES.includes(mode) ? mode : undefined;
        };
        const mode = normalize(entry.mode);
        const prev = normalize(entry.prev);
        if (mode === undefined || prev === undefined) continue;
        
  • hooks/genshijin-statusline.ps1GitHub
    Read the script
    $Utf8NoBom = New-Object System.Text.UTF8Encoding($false)
    [Console]::OutputEncoding = $Utf8NoBom
    
    $ClaudeDir = if ($env:CLAUDE_CONFIG_DIR) { $env:CLAUDE_CONFIG_DIR } else { Join-Path $HOME ".claude" }
    $Flag = Join-Path $ClaudeDir ".genshijin-active"
    if (-not (Test-Path $Flag)) { exit 0 }
    
    # reparse point (symlink / junction) とサイズ超過ファイル拒否
    # 攻撃者がフラグを secret ファイルへの symlink に置換した場合、
    # 描画毎にその内容(ANSI エスケープ含む)が端末に流れる攻撃を塞ぐ
    try {
        $Item = Get-Item -LiteralPath $Flag -Force -ErrorAction Stop
        if ($Item.Attributes -band [System.IO.FileAttributes]::ReparsePoint) { exit 0 }
        if ($Item.Length -gt 64) { exit 0 }
    } catch {
        exit 0
    }
    
    $Mode = ""
    try {
        $Raw = Get-Content -LiteralPath $Flag -TotalCount 1 -ErrorAction Stop
        if ($null -ne $Raw) { $Mode = ([string]$Raw).Trim() }
    } catch {
        exit 0
    }
    
    # [a-z0-9-] 以外除去 → ホワイトリスト検証
    $Mode = $Mode.ToLowerInvariant()
    $Mode = ($Mode -replace '[^a-z0-9-]', '')
    
    $Valid = @('off','polite','normal','extreme','commit','review','compress','help')
    if (-not ($Valid -contains $Mode)) { exit 0 }
    if ($Mode -eq 'off') { exit 0 }
    
    # ASCII モード → 日本語ラベル
    $Label = switch ($Mode) {
        'polite'   { '丁寧' }
        'normal'   { '' }
        'extreme'  { '極限' }
        'commit'   { 'コミット' }
        'review'   { 'レビュー' }
        'compress' { '圧縮' }
        'help'     { 'ヘルプ' }
        default    { '' }
    }
    
    $Esc = [char]27
    if ([string]::IsNullOrEmpty($Label)) {
        [Console]::Write("${Esc}[38;5;172m[原始人]${Esc}[0m")
    } else {
        [Console]::Write("${Esc}[38;5;172m[原始人:$Label]${Esc}[0m")
    }
    
    # /genshijin-stats が書出した savings suffix (存在時のみ)
    $SuffixPath = Join-Path $ClaudeDir ".genshijin-statusline-suffix"
    if (Test-Path $SuffixPath) {
        try {
            $SuffixItem = Get-Item -LiteralPath $SuffixPath -Force -ErrorAction Stop
            if (-not ($SuffixItem.Attributes -band [System.IO.FileAttributes]::ReparsePoint) -and $SuffixItem.Length -le 32) {
                $Suffix = (Get-Content -LiteralPath $SuffixPath -Raw -ErrorAction Stop).Trim()
                if ($Suffix) {
                    [Console]::Write(" ${Esc}[38;5;108m$Suffix${Esc}[0m")
                }
            }
        } catch {}
    }
    
  • hooks/genshijin-statusline.shGitHub
    Read the script
    #!/bin/bash
    # genshijin — Claude Code statusline バッジスクリプト。
    # genshijin モードフラグを読み、色付きバッジ出力。
    #
    # ~/.claude/settings.json 使用例:
    #   "statusLine": { "type": "command", "command": "bash /path/to/genshijin-statusline.sh" }
    
    FLAG="${CLAUDE_CONFIG_DIR:-$HOME/.claude}/.genshijin-active"
    
    # symlink 拒否 — 攻撃者が ~/.ssh/id_rsa 等を指す symlink に置換できた場合、
    # バッジ描画の度に secret バイト(ANSI エスケープ含む)を端末に描画してしまう。
    [ -L "$FLAG" ] && exit 0
    [ ! -f "$FLAG" ] && exit 0
    
    # 64 バイトで読込を打ち切り、[a-z0-9-] 以外を除去
    MODE=$(head -c 64 "$FLAG" 2>/dev/null | tr -d '\n\r' | tr '[:upper:]' '[:lower:]')
    MODE=$(printf '%s' "$MODE" | tr -cd 'a-z0-9-')
    
    # ホワイトリスト — 該当なしは何も描画しない
    case "$MODE" in
      off|polite|normal|extreme|commit|review|compress|help) ;;
      *) exit 0 ;;
    esac
    [ "$MODE" = "off" ] && exit 0
    
    # ASCII モード → 日本語ラベル
    case "$MODE" in
      polite)   LABEL="丁寧" ;;
      normal)   LABEL="" ;;   # デフォルトは suffix なし
      extreme)  LABEL="極限" ;;
      commit)   LABEL="コミット" ;;
      review)   LABEL="レビュー" ;;
      compress) LABEL="圧縮" ;;
      help)     LABEL="ヘルプ" ;;
      *)        LABEL="" ;;
    esac
    
    if [ -z "$LABEL" ]; then
      printf '\033[38;5;172m[原始人]\033[0m'
    else
      printf '\033[38;5;172m[原始人:%s]\033[0m' "$LABEL"
    fi
    
    # /genshijin-stats が書出した savings suffix を末尾に付加 (存在時のみ)
    SUFFIX_PATH="${CLAUDE_CONFIG_DIR:-$HOME/.claude}/.genshijin-statusline-suffix"
    if [ -f "$SUFFIX_PATH" ] && [ ! -L "$SUFFIX_PATH" ]; then
      SUFFIX=$(head -c 32 "$SUFFIX_PATH" 2>/dev/null | tr -d '\n\r')
      if [ -n "$SUFFIX" ]; then
        printf ' \033[38;5;108m%s\033[0m' "$SUFFIX"
      fi
    fi
    
  • hooks/install.ps1GitHub
  • hooks/install.shGitHub
  • hooks/uninstall.ps1GitHub
  • hooks/uninstall.shGitHub

All 10 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 withgenshijin

なぜ多くトークン使う?少なくて済む🗿 Claude Code 公式プラグインディレクトリに公開済み。/plugin install genshijin でワンライナーインストール。 Claude Code / Codex 向けの超圧縮コミュニケーションスキル。caveman の日本語版をベースに、日本語特有の冗長表現に最適化。 トークン使用量を 約75%削減 しつつ、技術的正確性は100%維持。

Get the whole plugin