| name | build-rule |
| description | Use when user asks to 'build rule', 'create rule', 'author rule', 'write rule', 'add enforcement'. Interactive guide for authoring Gobby rule YAML definitions. |
| version | 1.0.0 |
| category | authoring |
| triggers | build rule, create rule, author rule, write rule, add enforcement |
| metadata | {"gobby":{"audience":"interactive","depth":0}} |
/gobby build-rule — Rule Authoring Skill
Guide users through authoring Gobby rule YAML definitions. Asks what behavior to enforce, which event triggers it, what effect to apply, and generates valid YAML with proper conditions and priority.
Workflow Overview
- Identify Behavior — What should be enforced or tracked?
- Choose Event — When should the rule fire?
- Choose Effect — What should happen?
- Write Conditions — When should the effect apply?
- Generate YAML — Produce the rule definition
- Validate & Install — Check and import
Step 1: Identify Behavior
Ask the user:
- "What behavior do you want to enforce or track?" — One sentence.
- "Should it block an action, track state, inject guidance, call a tool, or observe?"
Common behaviors and their effect types:
| Behavior | Effect Type |
|---|
| Prevent dangerous commands | block |
| Require a prerequisite before an action | block |
| Track what the agent has done | set_variable |
| Count occurrences of something | set_variable |
| Add guidance to the system message | inject_context |
| Auto-run a tool at session start | mcp_call |
| Log tool usage for analytics | observe |
Step 2: Choose Event
Ask: "When should this rule fire?"
| Event | When It Fires | Best For |
|---|
before_tool | Before any tool call | Blocking tools, tracking tool usage |
after_tool | After a tool completes | Tracking results, setting flags on success |
before_agent | Before each agent turn | Injecting context, checking messages |
session_start | When session begins | Initialization, importing data |
session_end | When session ends | Cleanup, exporting data |
stop | When agent tries to stop | Preventing premature exit |
pre_compact | Before context compaction | Saving state before memory loss |
Most common combinations:
| Goal | Event + Effect |
|---|
| Block a tool | before_tool + block |
| Block stop | stop + block |
| Track tool usage | after_tool + set_variable |
| Inject context per turn | before_agent + inject_context |
| Auto-run tool at start | session_start + mcp_call |
| Save state before compact | pre_compact + mcp_call |
Step 3: Choose Effect
block — Prevent an Action
effect:
type: block
tools: [Edit, Write]
mcp_tools: ["gobby-tasks:close_task"]
command_pattern: "git\\s+push"
command_not_pattern: "git\\s+push\\s+--dry-run"
reason: "You can't do this because..."
Tool matching:
tools — Native tools: Edit, Write, Bash, NotebookEdit, mcp__gobby__call_tool
mcp_tools — MCP tools: "server:tool" format. Supports "server:*" wildcards.
command_pattern / command_not_pattern — Only for Bash tool. Regex patterns.
- No tools/mcp_tools specified → blocks ALL tools for the event.
set_variable — Update State
effect:
type: set_variable
variable: my_flag
value: true
Expression detection: If the value is a string containing variables., .get(, +, and, or, len(, etc., it's evaluated as an expression.
value: true
value: 0
value: "hello"
value: "variables.get('counter', 0) + 1"
value: "variables.get('my_list', []) + ['new_item']"
value: "True if event.data.get('tool_name') == 'Edit' else False"
inject_context — Add to System Message
effect:
type: inject_context
template: |
## My Custom Guidance
You should do X because Y.
Current state: {{ my_variable }}
Templates support Jinja2: {{ var }}, {{ var | default('') }}, {{ list | join(', ') }}.
mcp_call — Trigger a Tool
effect:
type: mcp_call
server: gobby-memory
tool: sync_import
arguments:
session_id: "{{ session_id }}"
background: true
observe — Record an Observation
effect:
type: observe
category: "tool_usage"
message: "Tool {{ event.data.tool_name }} used"
Step 4: Write Conditions
Ask: "Should this rule always fire, or only under certain conditions?"
Condition Syntax
Conditions use SafeExpressionEvaluator — safe AST-based evaluation.
when: "not task_claimed"
when: "variables.get('stop_attempts', 0) < 3"
when: "event.data.get('tool_name') == 'Bash'"
when: "event.data.get('mcp_tool') == 'close_task'"
when: "'/tests/' not in tool_input.get('file_path', '')"
when: >-
task_claimed
and not plan_mode
and variables.get('mode_level', 2) >= 1
when: "tool_input.get('file_path', '').endswith('.py')"
when: "event.data.get('source') in ['clear', 'compact']"
Available in Conditions
| Variable | Available When | Description |
|---|
variables | Always | Session variables dict |
| Top-level vars | Always | Flattened session variables (e.g., task_claimed) |
event | Always | Hook event object |
event.data | Always | Event-specific data |
tool_input | before_tool, after_tool | Tool arguments dict |
source | session_start | Event source string |
Built-in Helper Functions
when: "task_tree_complete(variables.get('session_task'))"
when: "task_needs_human_review(variables.get('auto_task_ref'))"
when: "has_stop_signal(session_id)"
when: "mcp_called('gobby-memory', 'recall_with_synthesis')"
when: "not mcp_failed('gobby-tasks', 'validate_task')"
when: "is_server_listed(tool_input)"
when: "is_tool_unlocked(tool_input)"
when: "any(p in event.data.get('tool_input', {}).get('command', '') for p in ['git ', 'pytest'])"
when: "all(v > 0 for v in variables.get('scores', []))"
The evaluator supports: len, bool, str, int, list, dict, any, all,
plus generator expressions, list comprehensions, dict literals ({}), and ternary expressions.
Step 5: Generate YAML
Single Rule
group: my-custom-rules
tags: [custom]
rules:
my-rule-name:
description: "What this rule does"
event: before_tool
priority: 100
when: "condition expression"
effect:
type: block
tools: [Bash]
reason: "Why this is blocked"
Multi-Effect Rule
group: my-custom-rules
tags: [custom]
rules:
my-complex-rule:
description: "Track and block in one rule"
event: before_tool
priority: 30
when: "base condition"
effects:
- type: set_variable
variable: attempt_count
value: "variables.get('attempt_count', 0) + 1"
- type: inject_context
when: "variables.get('attempt_count', 0) > 2"
template: "You've attempted this {{ attempt_count }} times."
- type: block
tools: [Bash]
reason: "Blocked after too many attempts."
Multiple Rules in One Group
group: my-enforcement
tags: [custom, enforcement]
rules:
track-edits:
description: "Track file edits"
event: after_tool
when: "event.data.get('tool_name') in ['Edit', 'Write']"
effect:
type: set_variable
variable: files_edited
value: "variables.get('files_edited', 0) + 1"
require-commit:
description: "Require commit before stop if files were edited"
event: stop
when: "variables.get('files_edited', 0) > 0 and not variables.get('committed', False)"
effect:
type: block
reason: "You edited {{ files_edited }} files. Commit before stopping."
Priority Guidelines
| Range | Purpose |
|---|
| 5–10 | State initialization (counters, flags) |
| 10–20 | Primary blocking gates |
| 20–30 | Secondary enforcement |
| 30–50 | Tracking and TDD |
| 50+ | Context injection and MCP calls |
| 100 | Default (most custom rules) |
agent_scope
Scope rules to specific agent types:
rules:
no-push-workers:
event: before_tool
agent_scope: [developer, pipeline-worker, qa-reviewer]
effect:
type: block
tools: [Bash]
command_pattern: "git\\s+push"
reason: "Workers don't push."
Step 6: Validate & Install
Validation Checklist
- Group name is kebab-case —
my-rules, not myRules.
- Rule names are kebab-case —
no-push, not noPush.
- Event is valid — One of:
before_tool, after_tool, before_agent, session_start, session_end, stop, pre_compact.
- Effect type is valid — One of:
block, set_variable, inject_context, mcp_call, observe.
- Block effects have
reason — Required field.
- set_variable effects have
variable and value — Both required.
- inject_context effects have
template — Required field.
- mcp_call effects have
server and tool — Both required.
- Conditions use correct syntax —
variables.get('key', default) not variables['key'].
- Regex patterns are properly escaped —
\\s+ not \s+ (YAML double-escaping).
- Multi-effect rules use
effects (plural) — Not effect with a list.
- At most one block per rule — Multi-effect rules can only have one block effect.
- Block effects match the event —
tools matching is only useful for before_tool.
Install
gobby rules import my-rules.yaml
Or via MCP:
get_tool_schema("gobby-workflows", "create_rule")
call_tool("gobby-workflows", "create_rule", {
"name": "my-rule",
"group": "my-group",
"definition": { ... }
})
Tell the user:
Rule installed! To verify:
gobby rules list --group my-group
gobby rules show my-rule-name
To test, trigger the event and check the audit log:
gobby rules audit --limit 5
Common Patterns
Block a specific command
no-force-push:
event: before_tool
effect:
type: block
tools: [Bash]
command_pattern: "git\\s+push\\s+.*--force"
reason: "Force push is not allowed."
Require a prerequisite
require-tests-before-commit:
event: before_tool
when: "not variables.get('tests_passed', False)"
effect:
type: block
tools: [Bash]
command_pattern: "git\\s+commit"
reason: "Run tests before committing."
Count and gate
count-attempts:
event: stop
priority: 10
effect:
type: set_variable
variable: stop_attempts
value: "variables.get('stop_attempts', 0) + 1"
block-after-threshold:
event: stop
priority: 50
when: "variables.get('stop_attempts', 0) < 5"
effect:
type: block
reason: "Complete your work before stopping."
Inject context conditionally
inject-tdd-reminder:
event: before_agent
when: "variables.get('enforce_tdd') and variables.get('task_claimed')"
effect:
type: inject_context
template: |
## TDD Mode Active
Write tests BEFORE implementation code.
Auto-run tool on session start
import-memories:
event: session_start
priority: 30
effect:
type: mcp_call
server: gobby-memory
tool: sync_import
background: true
Key Gotchas
- First block wins — If multiple rules match, only the first block (by priority) fires. Other rules after it don't run.
- Block effects fail closed — If the condition errors, a block effect defaults to BLOCKING. Be careful with complex conditions.
- Other effects fail open — If the condition errors, non-block effects are SKIPPED.
- Variables mutate in-place — A
set_variable in rule 1 (priority 10) is visible to rule 2 (priority 20) in the same evaluation pass.
- YAML regex needs double escaping —
\\s+ in YAML becomes \s+ in the regex engine.
- Templates are Jinja2 —
{{ var }} in reason and template fields. Use {{ var | default('') }} for safety.
mcp_tools uses "server:tool" format — Not just the tool name.
- Rules are templates until installed — Files in
src/gobby/install/shared/rules/ are synced but disabled. Import and enable your custom rules.
See Also