// Hero + install block + real ClooksHook snippet function InstallBlock({ cmd, accent, autoType = true, lines = [] }) { // Three-step install: add marketplace → install+enable plugin → scaffold const steps = [ { cmd, output: [ ['→ Added marketplace ', ['muted', 'codestripes-dev/clooks-marketplace']], ], doneLabel: '✓ added.', typeSpeed: 12, runMs: 500, }, { cmd: 'claude plugin install clooks@clooks-marketplace', output: [ ['→ Installed ', ['muted', 'clooks@1.2.0'], ', enabled in this project.'], ], doneLabel: '✓ enabled.', typeSpeed: 12, runMs: 500, }, { cmd: '/clooks:setup', output: [ ['→ Created ', ['code', '.clooks/clooks.yml'], ', ', ['code', '.clooks/hooks/'], ', ', ['code', '.clooks/lockfile.json']], ['→ Installed ', ['muted', 'clooks-core-hooks@1.2.0'], ' (3 hooks)'], ], doneLabel: '✓ ready.', typeSpeed: 14, runMs: 600, }, ]; const [copied, setCopied] = React.useState(false); // For each step: { typed: string, phase: 'idle'|'typing'|'running'|'done' } const [state, setState] = React.useState( steps.map((s, i) => ({ typed: autoType ? '' : s.cmd, phase: autoType ? (i === 0 ? 'typing' : 'idle') : 'done' })) ); const setStep = React.useCallback((i, patch) => { setState(prev => prev.map((s, idx) => idx === i ? { ...s, ...patch } : s)); }, []); // Drive each step's typing + running React.useEffect(() => { if (!autoType) return; const active = state.findIndex(s => s.phase === 'typing'); if (active === -1) return; const { cmd: c, typeSpeed, runMs } = steps[active]; let i = state[active].typed.length; const iv = setInterval(() => { i++; setStep(active, { typed: c.slice(0, i) }); if (i >= c.length) { clearInterval(iv); setTimeout(() => setStep(active, { phase: 'running' }), 300); setTimeout(() => { setStep(active, { phase: 'done' }); if (active + 1 < steps.length) { setTimeout(() => setStep(active + 1, { phase: 'typing' }), 500); } }, 300 + runMs); } }, typeSpeed); return () => clearInterval(iv); // eslint-disable-next-line react-hooks/exhaustive-deps }, [state.map(s => s.phase).join(',')]); const copy = async () => { const parts = steps.map((s, i) => i === steps.length - 1 ? `claude ${s.cmd}` : s.cmd); const text = parts.join(' && '); let ok = false; try { if (navigator.clipboard && window.isSecureContext) { await navigator.clipboard.writeText(text); ok = true; } } catch {} if (!ok) { // Fallback for sandboxed iframes where clipboard API is blocked try { const ta = document.createElement('textarea'); ta.value = text; ta.style.position = 'fixed'; ta.style.top = '-9999px'; ta.setAttribute('readonly', ''); document.body.appendChild(ta); ta.select(); ok = document.execCommand('copy'); document.body.removeChild(ta); } catch {} } if (ok) { setCopied(true); setTimeout(() => setCopied(false), 1800); } }; return (
~/projects/my-repo
{steps.map((step, si) => { const s = state[si]; if (s.phase === 'idle') return null; const showCaret = s.phase === 'typing'; const showOutput = s.phase === 'running' || s.phase === 'done'; const showDone = s.phase === 'done'; return (
$ {s.typed} {showCaret && ( )}
{showOutput && (
{step.output.map((line, i) => (
{line.map((seg, j) => { if (typeof seg === 'string') return {seg}; const [kind, val] = seg; if (kind === 'muted') return {val}; if (kind === 'code') return {val}; return {val}; })}
))} {showDone && (
{step.doneLabel}
)}
)}
); })}
); } function HookSnippet({ compact = false }) { // Real ClooksHook object shape (not definePreToolUse) const lines = [ [[TK.com, '// .clooks/hooks/no-rm-rf.ts']], [[TK.kw, 'import type'], [TK.op, ' { '], [TK.ty, 'ClooksHook'], [TK.op, ' } '], [TK.kw, 'from'], [TK.str, " 'clooks'"]], '', [[TK.kw, 'export const'], [TK.fn, ' hook'], [TK.op, ': '], [TK.ty, 'ClooksHook'], [TK.op, ' = {']], [' ', [TK.prop, 'meta'], [TK.op, ': {']], [' ', [TK.prop, 'name'], [TK.op, ': '], [TK.str, "'no-rm-rf'"], [TK.op, ',']], [' ', [TK.prop, 'description'], [TK.op, ': '], [TK.str, "'Block destructive rm commands.'"], [TK.op, ',']], [' ', [TK.op, '},']], '', [' ', [TK.fn, 'PreToolUse'], [TK.op, '('], [TK.ty, 'ctx'], [TK.op, ') {']], [' ', [TK.kw, 'if'], [TK.op, ' ('], [TK.ty, 'ctx'], [TK.op, '.tool '], [TK.op, '!== '], [TK.str, "'Bash'"], [TK.op, ') '], [TK.kw, 'return'], [TK.op, ' { '], [TK.prop, 'result'], [TK.op, ': '], [TK.str, "'skip'"], [TK.op, ' }']], '', [' ', [TK.kw, 'const'], [TK.fn, ' cmd '], [TK.op, '= '], [TK.ty, 'ctx'], [TK.op, '.input.command '], [TK.op, '?? '], [TK.str, "''"]], [' ', [TK.kw, 'const'], [TK.fn, ' dangerous '], [TK.op, '= /'], [TK.str, 'rm\\s+-rf?\\s+(\\/|~|\\$HOME)'], [TK.op, '/.test(cmd)']], '', [' ', [TK.kw, 'return'], [TK.fn, ' dangerous'],], [' ', [TK.op, '? { '], [TK.prop, 'result'], [TK.op, ': '], [TK.str, "'block'"], [TK.op, ', '], [TK.prop, 'reason'], [TK.op, ': '], [TK.str, "`refusing: ${cmd}`"], [TK.op, ' }']], [' ', [TK.op, ': { '], [TK.prop, 'result'], [TK.op, ': '], [TK.str, "'allow'"], [TK.op, ' }']], [' ', [TK.op, '},']], [[TK.op, '}']], ]; return ( ); } function HeroCode({ tweaks }) { return (
v0.0.1 · pre-release · Claude Code

A TypeScript hook runtime
for Claude Code.

Write hooks once, run them safely, share them across projects and teams. Clooks wraps Claude Code's native hooks — so a crashed hook blocks the action instead of silently passing through.

macOS · Linux Compiled Bun binary MIT license
A hook, start to finish:
); } function HeroSplit({ tweaks }) { return (
v0.0.1 · pre-release

TypeScript hooks
for Claude Code.

Write hooks once, run them safely, share them across projects and teams. Clooks wraps native hooks so a crash blocks — it doesn't pass through.

macOS · Linux Compiled Bun binary MIT license
); } Object.assign(window, { InstallBlock, HookSnippet, HeroCode, HeroSplit });