sandfield-add-rule

Add a custom guardrail rule to .sandfield/hooks/rules.local.js. Interactive — asks for pattern + reason, validates regex, tests against the recent shell history, then appends. Repo-local rules persist across npx skills update. Use when user says "add a guardrail", "block this command", "add a hook rule", or invokes /sandfield-add-rule.

ryan-mardiono-sal/skills3 installsMITSynced Aug 26

Works with

Claude CodeCursorCodex CLIGitHub CopilotGemini CLI

Agent Skills format with YAML frontmatter. Claude Code reads it as-is.

---
name: "sandfield-add-rule"
description: "Add a custom guardrail rule to .sandfield/hooks/rules.local.js. Interactive — asks for pattern + reason, validates regex, tests against the recent shell history, then appends. Repo-local rules persist across npx skills update. Use when user says \"add a guardrail\", \"block this command\", \"add a hook rule\", or invokes /sandfield-add-rule."
license: "MIT"
---

# Sandfield — Add Rule

Add one repo-local guardrail rule that the stock `sandfield-hooks` `guardrails.js` will load and enforce. Written to `.sandfield/hooks/rules.local.js` — never overwritten by `npx skills update`. Versioned in the repo so the whole team gets the same rules on next pull.

## Pre-flight

### 1. Verify sandfield-hooks is installed

Check for:

- `.sandfield/hooks/guardrails.js` — must exist. If missing, tell the user:
  > "`sandfield-hooks` isn't installed in this repo. Run `/sandfield-hooks` first to scaffold the hook infrastructure."
  and stop.

- Verify `guardrails.js` is recent enough to load local rules. Grep for `LOCAL_RULES` and `rules.local.js` in the file. If neither found, the user has an old version:
  > "Your installed `guardrails.js` is older than the local-rules loader. Re-run `/sandfield-hooks` to update it (your `rules.local.js` will be preserved)."
  and stop.

### 2. Read existing rules (if any)

If `.sandfield/hooks/rules.local.js` exists, read it. Show the user the current rule list (one line each):

```
Current local rules (3):
  1. /dotnet\s+publish.*--configuration\s+Production/ — Direct prod publish blocked. Use the deploy pipeline.
  2. /bq\s+rm\s+-r/ — BigQuery recursive delete blocked.
  3. /az\s+storage\s+blob\s+delete-batch/ — Bulk blob delete blocked. Use the admin tool.
```

If no rules file yet, say so:
```
No local rules yet. This will create .sandfield/hooks/rules.local.js.
```

## Process — ask one question at a time

### Q1. What command pattern should be blocked?

> Enter a command pattern. Two formats accepted:
> - **Plain text** — `dotnet publish` will be auto-escaped and matched as a substring with word boundaries.
> - **Regex literal** — wrap in slashes: `/dotnet\s+publish.*Production/`. Flags supported: `i`, `m`, `s`. Example: `/RM\s+.*PROD/i`.

Determine which form. Decision logic:

- If input starts with `/` and ends with `/`, `/i`, `/m`, `/s`, or any combination → treat as regex literal. Parse out body + flags.
- Otherwise → treat as plain text. Escape regex metacharacters and wrap in `(^|\s)<escaped>(\s|$)` so it matches as a token, not a substring inside another word.

**Validate the regex.** Compile it inside a try/catch. If it throws, show the error and ask again.

**Sanity-check.** Reject patterns that are too broad — they will block everything and lock the user out:

- Pattern matches empty string → reject.
- Pattern length (after escaping) is < 3 chars → warn ("This pattern is very short and will match too aggressively — confirm with 'yes' to proceed").
- Pattern is `.*` or equivalent → reject outright.

### Q2. Why? (reason shown to the agent when blocked)

> One sentence. Will be returned to the AI agent (Claude / Copilot) as the deny reason, so phrase it so the agent can adjust its approach. Example: "Direct prod publish blocked. Use the deploy pipeline at `scripts/deploy.sh`."

Trim, enforce ≤200 chars, reject empty.

### Q3. Test the rule

Test the new rule against a sample set of commands. Source the sample from, in order of preference:

1. `git log --pretty=format:%s -n 50` — recent commit messages (proxy for what gets done in the repo).
2. PowerShell history: `Get-History | Select-Object -ExpandProperty CommandLine` (Windows) or `~/.bash_history` / `~/.zsh_history` (Unix).
3. If both unavailable, skip testing.

Run the new pattern against each sample. Show the user:

- Matches found (count + first 3 examples) → ask: "These commands would now be blocked. Proceed?"
- No matches → "Pattern compiled OK, no recent commands match. Proceed?"

If user says no, return to Q1.

### Q4. Append to `.sandfield/hooks/rules.local.js`

Read the existing file if present; create with the standard header if missing. Append the new rule to the exported array.

**Standard header** (use exactly this when creating the file):

```javascript
// .sandfield/hooks/rules.local.js
// Repo-local guardrail rules. Loaded by .sandfield/hooks/guardrails.js.
// Never overwritten by `npx skills update`. Versioned in the repo so the team shares the same rules.
// Add entries via `/sandfield-add-rule` or edit by hand.
//
// Each rule: { pattern: RegExp, reason: string }
//   - pattern matches against the full Bash command string the agent is about to run.
//   - reason is returned to the agent so it can adjust.

module.exports = [
];
```

Append the new rule inside the array, preserving existing entries. Format each rule on one line if it fits, multiline if not. Add a trailing comment with `// added YYYY-MM-DD` for traceability.

Example after appending:

```javascript
module.exports = [
  { pattern: /dotnet\s+publish.*--configuration\s+Production/, reason: 'Direct prod publish blocked. Use the deploy pipeline.' }, // added 2026-04-12
  { pattern: /bq\s+rm\s+-r/, reason: 'BigQuery recursive delete blocked.' }, // added 2026-05-03
];
```

**Validate after writing**: `node -e "require('./.sandfield/hooks/rules.local.js')"` to confirm the file parses. If it errors, restore the previous version (keep a backup in memory) and surface the error.

### Q5. Commit?

Ask:

> Commit the change? (`git add .sandfield/hooks/rules.local.js && git commit -m "guardrails: block <one-line summary of pattern>"`)
>
> - **yes** — run the commands
> - **no** — leave it staged for the user to commit manually

If yes, run the commands. Use the user's existing commit conventions (read recent log).

## Report

Print:

```
Added rule to .sandfield/hooks/rules.local.js:
  pattern: <the pattern>
  reason:  <the reason>

Local rules now: <count>
Stock rules:     10
Total enforced:  <total>

Effective on the next Bash invocation from Claude or Copilot. No restart needed.
```

## Editing or removing rules

This skill only adds. To edit or remove:

1. Open `.sandfield/hooks/rules.local.js` directly.
2. Edit the array.
3. Re-validate with `node -e "require('./.sandfield/hooks/rules.local.js')"`.

A future `sandfield-edit-rule` / `sandfield-remove-rule` skill could automate this — out of scope for now.

## What this skill is NOT

- Not for adding entirely new hooks (different event, different matcher). For that, edit `.claude/settings.json` and `.github/hooks/` directly, or re-run `/sandfield-hooks` to regenerate the wiring.
- Not for overriding stock rules. The stock rules in `guardrails.js` always apply. Local rules are additive.
- Not a debugger. To see why a command was blocked, run `node .sandfield/hooks/guardrails.js` with the command JSON on stdin manually.

More General & Other skills

← All General & Other skills

Check your AI visibility

One URL in, a 0–100 score and the exact fixes out.

RUN THE CHECK

Browse all the tools

15 tools across six categories
13 of them never send your data anywhere

Free · No signup · No trial clock

SEE THE DIRECTORY