From 0127fc8fd9a7a0b7279d46a4e642e224c27bc3cc Mon Sep 17 00:00:00 2001 From: Jeroen Schweitzer Date: Fri, 22 May 2026 12:37:47 +0200 Subject: [PATCH] chore(config): centralize git for teammates and enable tmux panes Two agent-team tooling changes: - teammateMode: in-process -> auto. Teammates now spawn in tmux split panes when the lead runs inside tmux, with graceful in-process fallback. - New PreToolUse hook git-centralize-guard.sh blocks .git-mutating commands (add, commit, merge, push, pull, rebase, reset, checkout, stash, cherry-pick, rm, mv) for teammates, keeping version control centralized to the lead. Detection keys on the agent_type field, which a teammate's hook input carries and the lead's does not. Read-only git is allowed. Documented in .claude/rules/git-safety.md. Hooks load at lead startup, so a restart is required for the hook to reach teammates. Co-Authored-By: Claude Opus 4.7 (1M context) --- .claude/hooks/git-centralize-guard.sh | 34 +++++++++++++++++++++++++++ .claude/rules/git-safety.md | 15 ++++++++++++ .claude/settings.json | 7 +++++- 3 files changed, 55 insertions(+), 1 deletion(-) create mode 100755 .claude/hooks/git-centralize-guard.sh diff --git a/.claude/hooks/git-centralize-guard.sh b/.claude/hooks/git-centralize-guard.sh new file mode 100755 index 000000000..84d3ebf2a --- /dev/null +++ b/.claude/hooks/git-centralize-guard.sh @@ -0,0 +1,34 @@ +#!/usr/bin/env bash +# git-centralize-guard: PreToolUse hook that blocks .git-mutating commands for +# agent-team teammates. Version control is centralized to the team lead +# (see CLAUDE.md / memory: "Team lead commits, never agents"). +# +# Detection: an agent-team teammate's hook input carries an "agent_type" field +# (e.g. "qatux"); the team lead's main session does NOT. So agent_type present +# => a teammate (or subagent) => block git writes. Absent => the lead => allow. +# (Empirically confirmed on Claude Code 2.1.148; docs field is "agent_type".) +# +# Read-only git (status, log, diff, show, fetch, branch listing) is always allowed. +set -uo pipefail + +INPUT=$(cat) + +# Only act on Bash tool calls +TOOL_NAME=$(echo "$INPUT" | python3 -c "import sys,json; print(json.load(sys.stdin).get('tool_name',''))" 2>/dev/null) +[ "$TOOL_NAME" = "Bash" ] || exit 0 + +# Teammates/subagents carry agent_type; the lead does not. +AGENT_TYPE=$(echo "$INPUT" | python3 -c "import sys,json; print(json.load(sys.stdin).get('agent_type','') or '')" 2>/dev/null) +[ -n "$AGENT_TYPE" ] || exit 0 # lead -> allow everything + +COMMAND=$(echo "$INPUT" | python3 -c "import sys,json; print(json.load(sys.stdin).get('tool_input',{}).get('command',''))" 2>/dev/null) + +# Block git history/working-tree-mutating subcommands anywhere in the command +# (catches chained forms like `cd x && git commit`). Read-only git is allowed. +if echo "$COMMAND" | grep -qE '(^|[^[:alnum:]_])git[[:space:]]+(add|commit|merge|push|pull|rebase|reset|checkout|stash|cherry-pick|rm|mv)([[:space:]]|$)'; then + echo "BLOCKED: git write operations are centralized to the team lead. Teammate '${AGENT_TYPE}' may not run: ${COMMAND}" >&2 + echo "Editing files is fine — leave staging, commits, merges, and pushes to the lead." >&2 + exit 2 +fi + +exit 0 diff --git a/.claude/rules/git-safety.md b/.claude/rules/git-safety.md index 0bb32471c..ab08e9e28 100644 --- a/.claude/rules/git-safety.md +++ b/.claude/rules/git-safety.md @@ -7,6 +7,21 @@ - Skip files in `.gitignore` - The `.claude/` directory IS tracked — skills and agents belong in the repo +## Centralized version control (agent teams) + +Git is the **team lead's** job. Teammates edit files; the lead stages, commits, +merges, and pushes. This is enforced, not just convention: + +- **Hook:** `.claude/hooks/git-centralize-guard.sh` (PreToolUse on Bash) blocks + `.git`-mutating commands — `add`, `commit`, `merge`, `push`, `pull`, `rebase`, + `reset`, `checkout`, `stash`, `cherry-pick`, `rm`, `mv` — for any agent-team + teammate. Read-only git (`status`, `log`, `diff`, `show`, `fetch`) is allowed. +- **Detection:** a teammate's PreToolUse hook input carries an `agent_type` field + (e.g. `"qatux"`); the lead's main session does not. Present → block; absent → + allow. (Confirmed on Claude Code 2.1.148.) +- **Activation:** hooks load at lead startup and teammates inherit that snapshot, + so changes to this hook require a Claude Code restart to reach teammates. + ## Commit conventions Use conventional commits: `(): ` diff --git a/.claude/settings.json b/.claude/settings.json index d75c2e88d..cee717f29 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -4,7 +4,7 @@ "CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS": "1", "SR_DB_PATH": "/var/home/jeroenschweitzer/Projects/settled-reach/settledreach.db" }, - "teammateMode": "in-process", + "teammateMode": "auto", "permissions": { "allow": [ "Bash(git add *)", @@ -87,6 +87,11 @@ "type": "command", "command": ".claude/hooks/git-lock-guard.sh", "timeout": 5 + }, + { + "type": "command", + "command": ".claude/hooks/git-centralize-guard.sh", + "timeout": 5 } ] }