Opt-in Claude Code hooks: refuse attribution lines in commits and PRs, refuse staging COMMITS.md, warn on commits in a main checkout, and restart idle sessions…

<img src="docs/assets/hero.png" width="420" alt="techne Hero Image">
A Claude Code plugin marketplace: skills for code repos and for documents, opt-in git guards, and a spoken Claude Code.
| Plugin | For | Runs in |
|---|---|---|
techne | Work in a code repo: audits, CI review, pre-merge review, commit plans, GitHub catch-up, cross-repo drift, slop and doc drift | Claude Code |
graphe | Documents: LaTeX papers, markdown-to-PDF, talk decks, paper scaffolds and novelty review | Claude Code |
dokimasia | Bibliography verification for LaTeX projects: lint a .bib or hand-written reference list, verify it against arXiv and Crossref, check every cited key rendered | Claude Code, or one standalone Python file |
phylax | Opt-in git and PR guards (a mod), restart onto an updated Claude Code | Claude Code |
keryx | Speaks a short gist of each reply in a local voice (WSL2) | Claude Code |
Install only the ones you use: every enabled plugin's skill list sits in Claude's context on every turn. graphe holds skills only, so claude.ai and Cowork can install it too, but its build and check steps run shell commands (uv, TeX Live, Typst) that need Claude Code.
| Skill | What it does |
|---|---|
techne:audit | Runs the repo's make targets in dependency order and reconciles terminal output against logs/dev-*.log archives. |
techne:auto-commit | Groups working-tree changes into a structured COMMITS.md plan for staged review before anything lands. |
techne:catchup | Reads every comment, review, and state change on a repo's issues and PRs since you last participated, then reports who is blocked on whom. |
techne:ci-audit | Audits GitHub Actions runs on the current branch/PR: surfaces warnings, failures, and noise; fixes what's fixable in-repo. |
techne:deslop | Scans comments and docstrings for AI-generated slop and proposes tightened rewrites. |
techne:docs-site | Maintains the Zensical-powered docs site: config, deploy pipeline, theming, link integrity. |
techne:docsync | Verifies documentation claims (CLI commands, paths, config keys, signatures) against the actual code. |
techne:elenchus | Adversarial pre-merge review: drives /code-review, then reproduces the load-bearing claim, traces every consumer across the whole repo, and walks a bug-class rubric for reachable destructive ops, unmirrored parallel-path guards, migration crashes, and dead-but-green features. |
techne:research-grounded | Flags design decisions in IMPL/ROADMAP that lack # research(YYYY-MM): provenance, then web-searches to ground them. |
techne:reslop | Rewrites docstrings grounded in the implementation rather than deleting them outright. |
techne:sisters | Cross-repo drift audit across the sister repos listed in ~/.claude/techne.toml. |
techne:theoros | Starts an observed live dev session: Claude drives the REPL in a named tmux session; you spectate read-only via tmux attach -r. |
| Skill | What it does |
|---|---|
graphe:latex | Builds a LaTeX document and gates it on its log, its PDF, and the assignment it answers; a clean latexmk exit is not the signal. |
graphe:paper | Scaffolds a new paper dir (LaTeX + results-harvest + shared bib + portfolio row) in a papers-style monorepo so it builds on day one. |
graphe:paper-review | Pre-submission novelty + reviewer pass for a draft paper: grounds every novelty/claim verdict in retrieved prior work, flags related-work gaps, and surfaces lab-overlap for disclosure. |
graphe:pdf | Renders markdown to print-quality PDFs through a Typst template, then verifies fonts and content against the source. |
graphe:slides | Gates a talk deck before it is presented: real slide titles, contrast, alt text, stray figures; renders it through the app that will show it. |
| Skill | What it does |
|---|---|
dokimasia:check | Lints any .bib or hand-written thebibliography offline, resolves its arXiv ids and DOIs against arXiv and Crossref and tells a failed lookup from a wrong entry, and checks that every cited key rendered in the built document. Configured per project, standard library only. |
Three opt-in guards on git commit, git add and gh pr, run by a mod (Claude Code 2.1.287 or later): block attribution lines, from the text Claude Code composes through to the command Claude runs; block staging COMMITS.md; and warn on commits in the main checkout. While a blocking guard is on, a check that fails refuses the command instead of letting it through. A fourth option, restart_on_update, moves an idle session onto an updated Claude Code in a new tmux window or Windows Terminal tab and keeps the conversation. Each is off until you switch it on in /config. See Configuration and Restart on update.
From inside Claude Code:
/plugin marketplace add ajbarea/techne
/plugin install techne@techne
/plugin install graphe@techne
/plugin install dokimasia@techne
/plugin install phylax@techne
/plugin install keryx@techne # WSL2 on Windows only
Invoke a skill as /techne:<name>, /graphe:<name> or /dokimasia:check, or describe the task and Claude picks the matching skill. Run /skills to confirm they loaded. On claude.ai, add ajbarea/techne under Customize > Plugins > Add > Add marketplace, then add graphe.
First-time setup: the techne skills are opinionated about a few conventions (Makefile pattern, dev-runner archive,
.claude/skill-context.md). See Conventions for the minimum each skill needs.
techne:sisters reads ~/.claude/techne.toml at runtime (user-controlled config that lists the active sister repos to compare against).
github_user = "your-github-username"
workspace_root = "/path/to/your/workspace"
[[sisters]]
name = "repo-one"
status = "active"
[[sisters]]
name = "repo-two"
status = "active"
[[sisters]]
name = "repo-three"
status = "active"
Set status = "backburner" to skip a repo without removing it.
~/.claude/techne.toml ← user-controlled sister-repo registry
│
▼
techne (marketplace: ajbarea/techne)
├── techne (plugin, plugins/techne)
│ ├── audit ── verifies build targets vs. logs/
│ ├── auto-commit ── groups diffs into COMMITS.md
│ ├── catchup ── who is blocked on whom since you last looked
│ ├── ci-audit ── reads gh runs, fixes warnings in-repo
│ ├── deslop ── flags AI-slop prose
│ ├── docs-site ── manages Zensical site + deploy
│ ├── docsync ── doc claims ↔ implementation
│ ├── elenchus ── adversarial pre-merge review (reproduce + trace + rubric)
│ ├── research-grounded ── flags un-grounded design decisions
│ ├── reslop ── rewrites docstrings from code
│ ├── sisters ── cross-repo drift across sisters
│ └── theoros ── observed tmux REPL session
├── graphe (plugin, plugins/graphe)
│ ├── latex ── builds LaTeX and gates the PDF on its log
│ ├── paper ── scaffolds a new paper dir (LaTeX + harvest)
│ ├── paper-review ── grounded novelty + reviewer pass for a draft
│ ├── pdf ── markdown to print PDF via Typst, verified
│ └── slides ── talk deck gated and rendered, presenter briefed
├── dokimasia (plugin, plugins/dokimasia)
│ └── check ── lints, verifies and render-checks a .bib
├── phylax (plugin, plugins/phylax) ── opt-in git guards + restart on update
└── keryx (plugin, ajbarea/keryx) ── spoken gist of each reply
Each skill is self-contained. Invoke one without pulling in the others. They share a convention of writing intermediate artifacts (plans, audit reports) to disk for human review before mutating the repo.
Greek τέχνη: craft, the practical knowledge of how to make a thing well. The plugins keep the Greek: γραφή (graphe) is writing, φύλαξ (phylax) a guard, κῆρυξ (keryx) a herald, δοκιμασία (dokimasia) the scrutiny of a candidate's credentials before office.
MIT.
<img src="docs/assets/brand.png" alt="" height="16" />
hooks/register.ts 78 lines1import type { PluginOptions, Register } from 'claude-code'
2
3// The guard options, as the manifest's userConfig names them.
4const GUARDS = ['block_attribution_trailers', 'block_commits_md', 'warn_main_checkout_commit'] as const
5// The guards that refuse a command; warn_main_checkout_commit only warns.
6const BLOCKING = ['block_attribution_trailers', 'block_commits_md'] as const
7// git_guards.py looks no further at a command without one of these words.
8const GIT_OR_GH = /\b(git|gh)\b/
9// A commit makes at most six git calls of 5 s each inside git_guards.py.
10const GUARD_TIMEOUT_MS = 40_000
11
12type Verdict = { deny?: string; warning?: string }
13
14const isOn = (options: PluginOptions, key: string): boolean => options[key] === true
15
16// Reads git_guards.py's PreToolUse JSON: a deny reason, or a warning for the model.
17export function readVerdict(stdout: string): Verdict {
18 if (stdout.trim() === '') return {}
19 const out = JSON.parse(stdout) as {
20 hookSpecificOutput?: { permissionDecision?: string; permissionDecisionReason?: string; additionalContext?: string }
21 }
22 const spec = out.hookSpecificOutput ?? {}
23 if (spec.permissionDecision === 'deny') return { deny: spec.permissionDecisionReason ?? 'phylax refused this command.' }
24 if (spec.additionalContext) return { warning: spec.additionalContext }
25 return {}
26}
27
28export const register: Register = (on, options) => {
29 // Claude Code reads the commit trailer and PR footer from here, so blanking them stops the
30 // lines before Claude writes them; the Bash guard below still catches one written by hand.
31 if (isOn(options, 'block_attribution_trailers')) {
32 on('attribution.text', { kind: ['commit', 'pr'] }, () => ({ text: '' }))
33 }
34
35 if (!GUARDS.some(key => isOn(options, key))) return
36 const blocking = BLOCKING.some(key => isOn(options, key))
37 const failed = (why: string) => ({
38 deny:
39 `phylax: the git guard ${why}, so this command was not run. Retry it in a simpler form, ` +
40 'or switch the guard off in /config.',
41 })
42
43 on('tool.call', { tool: 'Bash' }, async ($, e, next) => {
44 // A subagent's call goes to the hooks.json PreToolUse fallback, which gets its own cwd;
45 // $.session.cwd() is the main thread's.
46 if (e.agentId !== undefined || !GIT_OR_GH.test(e.command)) return next(e)
47 const cwd = await $.session.cwd()
48 const run = await $.process.run(['python3', `${$.plugin.root}/hooks/git_guards.py`], {
49 cwd,
50 stdin: JSON.stringify({ hook_event_name: 'PreToolUse', tool_name: 'Bash', tool_input: { command: e.command }, cwd }),
51 env: Object.fromEntries(GUARDS.map(key => [`CLAUDE_PLUGIN_OPTION_${key.toUpperCase()}`, String(isOn(options, key))])),
52 timeoutMs: GUARD_TIMEOUT_MS,
53 })
54 const verdict = readVerdict(run.stdout)
55 if (verdict.deny !== undefined) return { deny: verdict.deny }
56 if (run.exitCode !== 0) {
57 const reason = run.stderr.trim().split('\n')[0] || `exit ${run.exitCode}`
58 if (blocking) return failed(`failed (${reason})`)
59 $.ui.log(`phylax: the git guard failed (${reason}); the command ran unchecked.`)
60 }
61 // The hooks.json fallback runs inside next(e). Naming this call there makes it stand down
62 // for this call alone: any call this hook did not pass, it still checks.
63 if (e.tool_use_id !== undefined) await $.env.set('PHYLAX_GUARD_CHECKED', e.tool_use_id)
64 let result
65 try {
66 result = await next(e)
67 } finally {
68 await $.env.set('PHYLAX_GUARD_CHECKED', undefined)
69 }
70 if (verdict.warning === undefined || result.deny !== undefined) return result
71 $.ui.log(verdict.warning)
72 return { ...result, context: [...(result.context ?? []), verdict.warning] }
73 }).catch(($, e, next) => {
74 if (next.called || !blocking) return next(e)
75 return failed(`failed (${next.error.message})`)
76 })
77}
78