In my previous post, I covered contributions to Gastown’s codebase. This post focuses on configuration—how I run Gastown safely with role-based access control, Claude Code hooks, and guard scripts that enforce the principle of least privilege.
The Problem: Autonomous Agents Need Boundaries
Multi-agent systems present a safety challenge. Agents operate autonomously, making decisions and taking actions. Without constraints:
- A planning agent might accidentally commit code
- A monitoring agent might push to protected branches
- A worker agent might modify files outside its worktree
- Any agent might merge PRs without human approval
The solution is defense in depth—multiple layers of protection that don’t rely on agents following instructions.
Configuration Architecture
My Gastown configuration uses three layers:
| Layer | File | Purpose |
|---|---|---|
| Role Contexts | ~/gt/settings/config.json | Customizes agent behavior via system prompts |
| Claude Code Hooks | Per-agent .claude/settings.json | Triggers guard scripts before tool execution |
| Guard Scripts | ~/gt/.runtime/hooks/*.sh | Enforces rules at the tool level |
| Hook Registry | ~/gt/hooks/registry.toml | Centralized hook definitions for easy deployment |
Each layer provides independent protection. An agent might ignore its context instructions, but the guard scripts enforce rules regardless.
Layer 1: Role Context Overrides
Gastown’s settings/config.json lets you override the default role contexts with custom system prompts. My configuration establishes clear boundaries:
| |
Role Responsibilities
| Role | Purpose | Permissions |
|---|---|---|
| Mayor | Orchestrator, reviewer, coordinator | Read-only; approves polecat work |
| Crew | Planning, analysis, architecture | Read-only; prepares work for polecats |
| Polecat | Implementation | Can code in worktree; feature branches only |
| Witness | Monitoring, violation detection | Read-only; halts rule violations |
| Deacon | Daemon patrol, lifecycle management | Read-only; manages agent sessions |
| Refinery | Disabled | Does nothing; human handles merges |
Approval Workflow
Polecats don’t have free rein. They operate under an approval gate workflow:
1. Mayor assigns bead → gt sling <bead> <rig>
2. Polecat submits PLAN_SUBMISSION → Mayor reviews
3. Mayor sends PLAN_RESPONSE (approve/reject)
4. Polecat implements and sends REVIEW_REQUEST
5. Mayor inspects logs, sends FINAL_APPROVAL
6. Polecat pushes to feature branch (never main)
This keeps human-in-the-loop even with autonomous execution.
Layer 2: Claude Code Hooks
Context instructions are suggestions—agents can ignore them. The enforcement layer uses Claude Code’s PreToolUse hook to intercept tool calls before execution.
Important: Settings must be per-agent, not at the town root. Placing settings.json at ~/gt/.claude/ would pollute all child workspaces. Each agent has its own settings at locations like:
~/gt/mayor/.claude/settings.json~/gt/deacon/.claude/settings.json~/gt/<rig>/polecats/.claude/settings.json(inherited by all polecats in that rig)~/gt/<rig>/witness/.claude/settings.json
Example configuration for a non-coder role (mayor):
| |
Critical: Guards must exit with code 2 to block tool execution. Exit code 1 is treated as an error but doesn’t block. The blocking message must go to stderr for Claude to see it:
| |
Every Bash command passes through git-guard.sh. Every Edit/Write/NotebookEdit passes through file-guard.sh.
Layer 3: Guard Scripts
git-guard.sh
Enforces git workflow rules based on role. The script reads tool input from stdin as JSON and detects the role from the current working directory:
| |
file-guard.sh
Blocks direct file modifications for non-coder roles and enforces worktree boundaries for polecats:
| |
Protection Matrix
| Role | Edit/Write | Bash | Git Push | Git Commit |
|---|---|---|---|---|
| Polecat | Within worktree | All | Feature branches | Yes |
| Crew | Blocked | All | Blocked | Blocked |
| Mayor | Blocked | All | Blocked | Blocked |
| Deacon | Blocked | All | Blocked | Blocked |
| Witness | Blocked | All | Blocked | Blocked |
| Refinery | Blocked | Limited | Blocked | Blocked |
What’s Explicitly Allowed
The guards are surgical—they block code modification while preserving operational capabilities. Non-coder roles retain full access to coordination tools:
| Tool | Type | Allowed for Crew/Mayor? | Why |
|---|---|---|---|
bd create, bd update | Bash | Yes | Issue tracking is core to planning |
bd sync | Bash | Yes | Beads sync doesn’t modify code |
gt mail, gt nudge | Bash | Yes | Inter-agent communication |
gt sling, gt convoy | Bash | Yes | Work coordination |
mcp__memory__* | MCP | Yes | Knowledge graph has no hook |
mcp__sequential-thinking__* | MCP | Yes | Reasoning tools unaffected |
git status, git log | Bash | Yes | Read operations allowed |
Key insight: The hooks only match Bash, Edit, Write, and NotebookEdit. MCP tools like memory and sequential thinking have no hook matchers, so they pass through unrestricted. This is intentional—planning agents need to record decisions and reason through problems.
The guards block code modification, not coordination work:
| |
Model Selection by Role
Different roles benefit from different models. Planning and orchestration need stronger reasoning; execution can use faster models:
| |
Opus handles the thinking (mayor, crew). Sonnet handles the doing (polecats, patrol agents).
Custom Branch Workflow
I run a fork with a custom branch that bundles unreleased features from open PRs. This enables using improvements before upstream merge.
| |
When upstream merges a PR, I rebase custom to drop those commits. Extra maintenance, but necessary when running a production multi-agent setup.
Disabled Refinery
The default Gastown configuration has an active refinery that processes merge queues. I disable it completely:
| |
The merge decision stays with the human. Agents prepare work; humans approve and merge.
Verification
Test guards by simulating tool calls. The key test is verifying exit code 2 for blocked operations:
| |
Run tests after any configuration change to verify guards work correctly.
Hook Registry and Installation
Managing hooks across many agents is tedious. Gastown provides a registry system to centralize hook definitions and deploy them easily.
Registry Definition
Create ~/gt/hooks/registry.toml:
| |
Managing Hooks
| |
The registry makes it easy to add new guards, update existing ones, and deploy consistently across all agents.
Note: gt hooks install currently handles rig-level roles (polecat, crew, witness, refinery) but not town-level roles (mayor, deacon). Those require manual settings updates.
Key Takeaways
Defense in depth: Context instructions + hooks + guards. Each layer is independent.
Principle of least privilege: Each role can only do what it needs. Planning agents can’t code. Coders can’t merge.
Human stays in control: Approval workflows for implementation. Disabled refinery for merges.
Be surgical: Allow what’s needed (Bash for
gt/bdcommands), block what’s dangerous (Edit/Write for non-coders).Test your guards: Automated tests catch regressions when updating configuration.
The configuration files are available in my gastown fork on the custom branch.