@bookedsolid/reagent 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +118 -0
- package/agents/reagent-orchestrator.md +66 -0
- package/bin/init.js +818 -0
- package/commands/rea.md +76 -0
- package/commands/restart.md +105 -0
- package/cursor/rules/001-no-hallucination.mdc +28 -0
- package/cursor/rules/002-verify-before-act.mdc +28 -0
- package/cursor/rules/003-attribution.mdc +36 -0
- package/hooks/attribution-advisory.sh +74 -0
- package/hooks/dangerous-bash-interceptor.sh +287 -0
- package/hooks/env-file-protection.sh +110 -0
- package/hooks/secret-scanner.sh +229 -0
- package/husky/commit-msg.sh +50 -0
- package/husky/pre-commit.sh +57 -0
- package/husky/pre-push.sh +75 -0
- package/package.json +60 -0
- package/profiles/bst-internal.json +30 -0
- package/profiles/client-engagement.json +30 -0
- package/templates/CLAUDE.md +55 -0
package/commands/rea.md
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
You are REA — the Reactive Execution Agent. The active ingredient of reagent (`rea` + `gent` = `reagent`).
|
|
2
|
+
|
|
3
|
+
You are the AI orchestrator for this project. You govern the AI agent team, route tasks to specialists, evaluate the roster, and enforce zero-trust across all AI operations.
|
|
4
|
+
|
|
5
|
+
## First Steps — Every Invocation
|
|
6
|
+
|
|
7
|
+
1. Read `.reagent/policy.yaml` — confirm autonomy level and profile
|
|
8
|
+
2. Check for `.reagent/HALT` — if present, report FROZEN and stop
|
|
9
|
+
3. Identify the project context (read CLAUDE.md, check for `.clarity/` submodule, scan `.claude/agents/`)
|
|
10
|
+
|
|
11
|
+
## What you can do
|
|
12
|
+
|
|
13
|
+
Based on `$ARGUMENTS`, handle one of these:
|
|
14
|
+
|
|
15
|
+
### Team Status / Roster Review
|
|
16
|
+
|
|
17
|
+
If asked about team status, roster, or agents:
|
|
18
|
+
|
|
19
|
+
1. List all agents in `.claude/agents/` (including symlinked directories)
|
|
20
|
+
2. Count by category
|
|
21
|
+
3. Identify gaps, redundancies, or merge candidates
|
|
22
|
+
4. Evaluate using: Business Value (30%), Uniqueness (20%), Depth (20%), Zero-Trust Readiness (15%), Cross-Validation Ability (15%)
|
|
23
|
+
|
|
24
|
+
### Task Routing
|
|
25
|
+
|
|
26
|
+
If given a task to route:
|
|
27
|
+
|
|
28
|
+
1. Analyze the task requirements
|
|
29
|
+
2. Identify the best specialist agent(s) from the roster
|
|
30
|
+
3. Recommend delegation with rationale
|
|
31
|
+
4. Flag if no agent covers the need (gap analysis)
|
|
32
|
+
|
|
33
|
+
### Gap Analysis
|
|
34
|
+
|
|
35
|
+
If asked about gaps or missing capabilities:
|
|
36
|
+
|
|
37
|
+
1. Map current agent coverage against the project's domain needs
|
|
38
|
+
2. Identify uncovered domains
|
|
39
|
+
3. Propose new agents or merges with justification
|
|
40
|
+
4. Prioritize by business impact
|
|
41
|
+
|
|
42
|
+
### Agent Evaluation
|
|
43
|
+
|
|
44
|
+
If asked to evaluate specific agents:
|
|
45
|
+
|
|
46
|
+
1. Read the agent definition file
|
|
47
|
+
2. Score against the evaluation framework
|
|
48
|
+
3. Compare with overlapping agents
|
|
49
|
+
4. Recommend: keep, merge, retire, or enhance
|
|
50
|
+
|
|
51
|
+
### Zero-Trust Audit
|
|
52
|
+
|
|
53
|
+
If asked about zero-trust compliance:
|
|
54
|
+
|
|
55
|
+
1. Read agent definitions
|
|
56
|
+
2. Check for zero-trust DNA (source validation, no LLM memory trust, cross-validation, freshness, graduated autonomy, HALT compliance, audit awareness)
|
|
57
|
+
3. Flag non-compliant agents
|
|
58
|
+
4. Recommend fixes
|
|
59
|
+
|
|
60
|
+
### Health Check
|
|
61
|
+
|
|
62
|
+
If asked about health or status with no specific focus:
|
|
63
|
+
|
|
64
|
+
1. Read `.reagent/policy.yaml`
|
|
65
|
+
2. Check `.reagent/HALT`
|
|
66
|
+
3. Run `git status` — report branch, clean/dirty
|
|
67
|
+
4. Count agents by category
|
|
68
|
+
5. Report autonomy level and any constraints
|
|
69
|
+
|
|
70
|
+
## Constraints
|
|
71
|
+
|
|
72
|
+
- ALWAYS read `.reagent/policy.yaml` before taking action — respect autonomy levels
|
|
73
|
+
- ALWAYS check for `.reagent/HALT` before proceeding
|
|
74
|
+
- NEVER modify agent files without explicit approval — recommend changes, don't make them
|
|
75
|
+
- NEVER trust claims about agent capabilities without reading the definition file
|
|
76
|
+
- Present analysis with evidence, not opinions
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
Handle both session spin-down and spin-up for any reagent-managed project.
|
|
2
|
+
|
|
3
|
+
## Which mode to run — explicit args win, context as fallback
|
|
4
|
+
|
|
5
|
+
Check `$ARGUMENTS` first:
|
|
6
|
+
|
|
7
|
+
- `/restart down` → **spin-down** (save state, write RESTART.md, output restart prompt)
|
|
8
|
+
- `/restart up` → **spin-up** (read RESTART.md, orient, list Up Next)
|
|
9
|
+
- `/restart` with no args → infer from context:
|
|
10
|
+
- If this is clearly early in a fresh session (few messages, no code changes) → **spin-up**
|
|
11
|
+
- If there's meaningful work done this session → **spin-down**
|
|
12
|
+
- If ambiguous → **ask**: "Spin down (save state) or spin up (orient from last session)?"
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## SPIN-DOWN: Save state and prepare handoff
|
|
17
|
+
|
|
18
|
+
1. Read `.reagent/policy.yaml` to capture current autonomy level and profile
|
|
19
|
+
2. Run `git status` and `git log --oneline -10` to capture repo state
|
|
20
|
+
3. Review the conversation for completed work, in-progress items, and next steps
|
|
21
|
+
4. Rewrite `RESTART.md` (gitignored) in full:
|
|
22
|
+
|
|
23
|
+
```markdown
|
|
24
|
+
# Session Restart Context
|
|
25
|
+
|
|
26
|
+
_Last updated: [DATE]_
|
|
27
|
+
|
|
28
|
+
## Completed This Session
|
|
29
|
+
|
|
30
|
+
- [bullet list of everything finished — features, fixes, commits, changesets]
|
|
31
|
+
|
|
32
|
+
## In Progress
|
|
33
|
+
|
|
34
|
+
- [anything started but not committed/tested — or "Nothing in progress" if clean]
|
|
35
|
+
|
|
36
|
+
## Up Next
|
|
37
|
+
|
|
38
|
+
- [ordered list of immediate next steps for the new session]
|
|
39
|
+
|
|
40
|
+
## Pending Changesets / PRs
|
|
41
|
+
|
|
42
|
+
- [open changesets, staged changes, PRs awaiting review/merge — or "None"]
|
|
43
|
+
|
|
44
|
+
## Key Context & Decisions
|
|
45
|
+
|
|
46
|
+
- [important decisions made, constraints, gotchas, things not to forget]
|
|
47
|
+
|
|
48
|
+
## Repo State
|
|
49
|
+
|
|
50
|
+
- Branch: [current branch]
|
|
51
|
+
- Last commit: [hash + message]
|
|
52
|
+
- Working tree: [clean / list modified files]
|
|
53
|
+
- Autonomy level: [from policy.yaml]
|
|
54
|
+
- Profile: [from policy.yaml]
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
5. Output this exactly:
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
RESTART.md updated. To resume in a new session:
|
|
62
|
+
|
|
63
|
+
```
|
|
64
|
+
/restart
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Claude will read RESTART.md automatically and orient itself. Then confirm "Ready — here's where we left off" and list Up Next.
|
|
68
|
+
|
|
69
|
+
---
|
|
70
|
+
|
|
71
|
+
## SPIN-UP: Orient from saved state
|
|
72
|
+
|
|
73
|
+
1. Read `RESTART.md`
|
|
74
|
+
2. Read `.reagent/policy.yaml` — confirm autonomy level, check for HALT
|
|
75
|
+
3. Run these in parallel:
|
|
76
|
+
- `git status` and `git log --oneline -5` — verify repo state matches RESTART.md
|
|
77
|
+
- Check for `.reagent/HALT` — if present, report FROZEN status and reason
|
|
78
|
+
4. Note any drift or issues:
|
|
79
|
+
- New commits since RESTART.md was written
|
|
80
|
+
- Autonomy level changes
|
|
81
|
+
- HALT file present (agent operations blocked)
|
|
82
|
+
5. Output a brief orientation:
|
|
83
|
+
|
|
84
|
+
---
|
|
85
|
+
|
|
86
|
+
**Resuming session.**
|
|
87
|
+
|
|
88
|
+
**Health:** [✓ reagent active | ⚠ FROZEN: reason]
|
|
89
|
+
**Autonomy:** [L0-L3 from policy.yaml]
|
|
90
|
+
|
|
91
|
+
**Last session completed:**
|
|
92
|
+
[bullet summary from RESTART.md]
|
|
93
|
+
|
|
94
|
+
**In progress:**
|
|
95
|
+
[from RESTART.md, or "Nothing — clean state"]
|
|
96
|
+
|
|
97
|
+
**Up next:**
|
|
98
|
+
[ordered list from RESTART.md]
|
|
99
|
+
|
|
100
|
+
**Repo state:** branch `[branch]`, last commit `[hash] [message]`
|
|
101
|
+
[If drift detected: "Note: [X] new commits since RESTART.md was written"]
|
|
102
|
+
|
|
103
|
+
Ready to continue — say the word.
|
|
104
|
+
|
|
105
|
+
---
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Prohibits Claude from fabricating APIs, packages, file paths, or code that hasn't been verified to exist
|
|
3
|
+
globs: ["**/*"]
|
|
4
|
+
alwaysApply: true
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# No Hallucination
|
|
8
|
+
|
|
9
|
+
**"I'm not sure" is always better than guessing.**
|
|
10
|
+
|
|
11
|
+
## Rules
|
|
12
|
+
|
|
13
|
+
1. **Never invent APIs or function signatures.** If you haven't read the source or docs, say so. Do not guess method names, parameter shapes, or return types.
|
|
14
|
+
|
|
15
|
+
2. **Never invent file paths.** Before referencing a file path, verify it exists. Use the available file-read tools — do not construct paths from assumptions.
|
|
16
|
+
|
|
17
|
+
3. **Never invent package names.** If you're not certain a package exists on the registry, say so. Do not `npm install` or `composer require` a package you haven't confirmed exists.
|
|
18
|
+
|
|
19
|
+
4. **Never invent test results.** If you haven't run the tests, do not claim they pass. Run them first.
|
|
20
|
+
|
|
21
|
+
5. **Prefer "I need to check X" over confident-but-wrong statements.** Uncertainty stated explicitly is a recoverable state. Confident fabrication causes downstream rework.
|
|
22
|
+
|
|
23
|
+
## When to apply
|
|
24
|
+
|
|
25
|
+
- Before referencing any external API or library method
|
|
26
|
+
- Before writing import statements for unfamiliar packages
|
|
27
|
+
- Before asserting that a file, function, or type exists
|
|
28
|
+
- Before claiming tests pass or build succeeds without running them
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Requires Claude to read before writing, verify before installing, and confirm before destructive operations
|
|
3
|
+
globs: ["**/*"]
|
|
4
|
+
alwaysApply: true
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Verify Before Acting
|
|
8
|
+
|
|
9
|
+
**Read before writing. Verify before installing. Confirm before deleting.**
|
|
10
|
+
|
|
11
|
+
## Rules
|
|
12
|
+
|
|
13
|
+
1. **Read before writing.** Before editing a file, read the current version. Never overwrite without knowing what's there. Never assume file contents from memory.
|
|
14
|
+
|
|
15
|
+
2. **Verify package existence before installing.** Before running `npm install <pkg>`, `composer require <pkg>`, or any equivalent, confirm the package exists and its name is spelled correctly. Use the package registry, not your training data.
|
|
16
|
+
|
|
17
|
+
3. **Confirm before destructive operations.** Before `git reset`, `git clean`, file deletion, or database drops — state what you're about to do and why. Wait for acknowledgement if there's any doubt.
|
|
18
|
+
|
|
19
|
+
4. **Check current state before declaring it.** Before saying "the build passes" or "there are no TypeScript errors", run the check. Do not rely on previous results from earlier in the session.
|
|
20
|
+
|
|
21
|
+
5. **Verify tool availability before use.** Before running a CLI tool (docker, act, composer, drush, etc.), check that it's installed. Don't assume availability.
|
|
22
|
+
|
|
23
|
+
## When to apply
|
|
24
|
+
|
|
25
|
+
- Before any write or edit operation
|
|
26
|
+
- Before any `install`, `add`, `require`, or `update` command
|
|
27
|
+
- Before any destructive git operation
|
|
28
|
+
- Before any claim about current system state
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Prohibits AI attribution strings in commits, PR bodies, code comments, and client-facing content
|
|
3
|
+
globs: ["**/*"]
|
|
4
|
+
alwaysApply: true
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# No AI Attribution in Client-Facing Content
|
|
8
|
+
|
|
9
|
+
**Do not expose AI tooling in client git history, PR descriptions, or code comments.**
|
|
10
|
+
|
|
11
|
+
## What to remove
|
|
12
|
+
|
|
13
|
+
Remove these patterns from all commits, PR bodies, commit messages, and code:
|
|
14
|
+
|
|
15
|
+
- `Co-Authored-By: Claude <noreply@anthropic.com>`
|
|
16
|
+
- `Co-Authored-By: Claude ...` (any variant)
|
|
17
|
+
- `Generated with Claude Code`
|
|
18
|
+
- `🤖 Generated with [Claude Code](https://claude.com/claude-code)`
|
|
19
|
+
- `claude.ai` references in commit messages or PR descriptions
|
|
20
|
+
- AI assistant footers in PR templates
|
|
21
|
+
|
|
22
|
+
## Enforcement
|
|
23
|
+
|
|
24
|
+
- **commit-msg hook** strips attribution automatically from every commit message
|
|
25
|
+
- This rule governs PR bodies and code comments, which the hook does not cover
|
|
26
|
+
- Before running `gh pr create`, review the body — remove attribution strings manually
|
|
27
|
+
|
|
28
|
+
## What's allowed
|
|
29
|
+
|
|
30
|
+
- Internal tooling notes in `.claude/` config files (gitignored)
|
|
31
|
+
- Documentation about the AI workflow in team-facing docs (e.g., `CONTRIBUTING.md`) when explicitly approved
|
|
32
|
+
- Attribution in commits to internal/private BST repos where it's expected
|
|
33
|
+
|
|
34
|
+
## Why
|
|
35
|
+
|
|
36
|
+
Client projects are delivered work product. AI tooling attribution in git history creates unnecessary disclosure, may violate NDA scope, and makes commits less professional. The work speaks for itself.
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# PreToolUse hook: attribution-advisory.sh
|
|
3
|
+
# Fires BEFORE every Bash tool call.
|
|
4
|
+
# Advisory only (exit 0 + stderr warning) — CANNOT rewrite tool input.
|
|
5
|
+
#
|
|
6
|
+
# Detects when a gh pr create / gh pr edit command body may contain AI
|
|
7
|
+
# attribution strings and warns Claude to self-correct before submitting.
|
|
8
|
+
#
|
|
9
|
+
# Hook protocol: PreToolUse hooks can ONLY block (exit 2) or allow (exit 0).
|
|
10
|
+
# This hook exits 0 in advisory mode and exits 2 only when HALT is active.
|
|
11
|
+
# The commit-msg hook is the mechanical enforcement for git commits.
|
|
12
|
+
#
|
|
13
|
+
# Exit codes:
|
|
14
|
+
# 0 = allow (advisory mode — always, unless HALT is active)
|
|
15
|
+
# 2 = block (only when .reagent/HALT is present)
|
|
16
|
+
|
|
17
|
+
set -uo pipefail
|
|
18
|
+
|
|
19
|
+
# ── 1. Read ALL stdin immediately before doing anything else ──────────────────
|
|
20
|
+
INPUT=$(cat)
|
|
21
|
+
|
|
22
|
+
# ── 2. Dependency check ───────────────────────────────────────────────────────
|
|
23
|
+
if ! command -v jq >/dev/null 2>&1; then
|
|
24
|
+
printf 'REAGENT ERROR: jq is required but not installed.\n' >&2
|
|
25
|
+
printf 'Install: brew install jq OR apt-get install -y jq\n' >&2
|
|
26
|
+
exit 2
|
|
27
|
+
fi
|
|
28
|
+
|
|
29
|
+
# ── 3. HALT check ─────────────────────────────────────────────────────────────
|
|
30
|
+
REAGENT_ROOT="${CLAUDE_PROJECT_DIR:-$(pwd)}"
|
|
31
|
+
HALT_FILE="${REAGENT_ROOT}/.reagent/HALT"
|
|
32
|
+
if [ -f "$HALT_FILE" ]; then
|
|
33
|
+
printf 'REAGENT HALT: %s\nAll agent operations suspended. Run: reagent unfreeze\n' \
|
|
34
|
+
"$(cat "$HALT_FILE" 2>/dev/null || echo 'Reason unknown')" >&2
|
|
35
|
+
exit 2
|
|
36
|
+
fi
|
|
37
|
+
|
|
38
|
+
# ── 4. Parse tool_input.command from the hook payload ─────────────────────────
|
|
39
|
+
CMD=$(printf '%s' "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null)
|
|
40
|
+
|
|
41
|
+
if [[ -z "$CMD" ]]; then
|
|
42
|
+
exit 0
|
|
43
|
+
fi
|
|
44
|
+
|
|
45
|
+
# ── 5. Only check gh pr commands ──────────────────────────────────────────────
|
|
46
|
+
if ! printf '%s' "$CMD" | grep -qiE 'gh[[:space:]]+pr[[:space:]]+(create|edit)'; then
|
|
47
|
+
exit 0
|
|
48
|
+
fi
|
|
49
|
+
|
|
50
|
+
# ── 6. Check for attribution strings in the command ───────────────────────────
|
|
51
|
+
FOUND_ATTRIBUTION=0
|
|
52
|
+
|
|
53
|
+
if printf '%s' "$CMD" | grep -qiE '(Co-Authored-By:[[:space:]]+Claude|Generated with Claude Code|claude\.ai|🤖[[:space:]]+Generated)'; then
|
|
54
|
+
FOUND_ATTRIBUTION=1
|
|
55
|
+
fi
|
|
56
|
+
|
|
57
|
+
if [[ $FOUND_ATTRIBUTION -eq 1 ]]; then
|
|
58
|
+
{
|
|
59
|
+
printf 'ATTRIBUTION ADVISORY: gh pr command may include AI attribution strings.\n'
|
|
60
|
+
printf '\n'
|
|
61
|
+
printf ' Detected AI attribution pattern in gh pr create/edit command.\n'
|
|
62
|
+
printf ' Review the PR body before proceeding — remove:\n'
|
|
63
|
+
printf ' - Co-Authored-By: Claude ...\n'
|
|
64
|
+
printf ' - Generated with Claude Code\n'
|
|
65
|
+
printf ' - 🤖 Generated with ...\n'
|
|
66
|
+
printf ' - claude.ai references\n'
|
|
67
|
+
printf '\n'
|
|
68
|
+
printf ' Note: commit-msg hook strips attribution from git commits automatically.\n'
|
|
69
|
+
printf ' PR bodies must be cleaned manually — the commit-msg hook does not cover them.\n'
|
|
70
|
+
} >&2
|
|
71
|
+
fi
|
|
72
|
+
|
|
73
|
+
# Always allow in advisory mode
|
|
74
|
+
exit 0
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# PreToolUse hook: dangerous-bash-interceptor.sh
|
|
3
|
+
# Fires BEFORE every Bash tool call.
|
|
4
|
+
# Detects destructive shell commands and blocks them (exit 2) or warns (exit 0).
|
|
5
|
+
#
|
|
6
|
+
# Compatible with: interactive sessions + headless Docker (no TTY required).
|
|
7
|
+
# All diagnostic output goes to stderr only.
|
|
8
|
+
#
|
|
9
|
+
# Content extraction:
|
|
10
|
+
# Bash tool → tool_input.command
|
|
11
|
+
#
|
|
12
|
+
# Exit codes:
|
|
13
|
+
# 0 = safe or advisory-only — allow the command to run
|
|
14
|
+
# 2 = HIGH severity danger detected — block the command with feedback
|
|
15
|
+
|
|
16
|
+
set -uo pipefail
|
|
17
|
+
|
|
18
|
+
# ── 1. Read ALL stdin immediately before doing anything else ──────────────────
|
|
19
|
+
INPUT=$(cat)
|
|
20
|
+
|
|
21
|
+
# ── 2. Dependency check ───────────────────────────────────────────────────────
|
|
22
|
+
if ! command -v jq >/dev/null 2>&1; then
|
|
23
|
+
printf 'REAGENT ERROR: jq is required but not installed.\n' >&2
|
|
24
|
+
printf 'Install: brew install jq OR apt-get install -y jq\n' >&2
|
|
25
|
+
exit 2
|
|
26
|
+
fi
|
|
27
|
+
|
|
28
|
+
# ── 3. HALT check ─────────────────────────────────────────────────────────────
|
|
29
|
+
REAGENT_ROOT="${CLAUDE_PROJECT_DIR:-$(pwd)}"
|
|
30
|
+
HALT_FILE="${REAGENT_ROOT}/.reagent/HALT"
|
|
31
|
+
if [ -f "$HALT_FILE" ]; then
|
|
32
|
+
printf 'REAGENT HALT: %s\nAll agent operations suspended. Run: reagent unfreeze\n' \
|
|
33
|
+
"$(cat "$HALT_FILE" 2>/dev/null || echo 'Reason unknown')" >&2
|
|
34
|
+
exit 2
|
|
35
|
+
fi
|
|
36
|
+
|
|
37
|
+
# ── 4. Parse tool_input.command from the hook payload ─────────────────────────
|
|
38
|
+
CMD=$(printf '%s' "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null)
|
|
39
|
+
|
|
40
|
+
if [[ -z "$CMD" ]]; then
|
|
41
|
+
exit 0
|
|
42
|
+
fi
|
|
43
|
+
|
|
44
|
+
# ── 5. Helper: truncate command for display ────────────────────────────────────
|
|
45
|
+
truncate_cmd() {
|
|
46
|
+
local STR="$1"
|
|
47
|
+
local MAX=200
|
|
48
|
+
if [[ ${#STR} -gt $MAX ]]; then
|
|
49
|
+
printf '%s' "${STR:0:$MAX}..."
|
|
50
|
+
else
|
|
51
|
+
printf '%s' "$STR"
|
|
52
|
+
fi
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
# ── 6. Violation accumulators ──────────────────────────────────────────────────
|
|
56
|
+
HIGH_FILE=$(mktemp "${TMPDIR:-/tmp}/reagent-bash-high-XXXXXX")
|
|
57
|
+
MEDIUM_FILE=$(mktemp "${TMPDIR:-/tmp}/reagent-bash-medium-XXXXXX")
|
|
58
|
+
|
|
59
|
+
cleanup_violations() {
|
|
60
|
+
rm -f "$HIGH_FILE" "$MEDIUM_FILE"
|
|
61
|
+
}
|
|
62
|
+
trap cleanup_violations EXIT
|
|
63
|
+
|
|
64
|
+
add_high() {
|
|
65
|
+
local LABEL="$1"
|
|
66
|
+
local DETAIL="$2"
|
|
67
|
+
shift 2
|
|
68
|
+
printf 'HIGH|%s|%s\n' "$LABEL" "$DETAIL" >> "$HIGH_FILE"
|
|
69
|
+
for ALT in "$@"; do
|
|
70
|
+
printf 'ALT:%s\n' "$ALT" >> "$HIGH_FILE"
|
|
71
|
+
done
|
|
72
|
+
printf 'END_VIOLATION\n' >> "$HIGH_FILE"
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
add_medium() {
|
|
76
|
+
local LABEL="$1"
|
|
77
|
+
local DETAIL="$2"
|
|
78
|
+
shift 2
|
|
79
|
+
printf 'MEDIUM|%s|%s\n' "$LABEL" "$DETAIL" >> "$MEDIUM_FILE"
|
|
80
|
+
for ALT in "$@"; do
|
|
81
|
+
printf 'ALT:%s\n' "$ALT" >> "$MEDIUM_FILE"
|
|
82
|
+
done
|
|
83
|
+
printf 'END_VIOLATION\n' >> "$MEDIUM_FILE"
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
# ── 7. Per-segment evaluation helper ──────────────────────────────────────────
|
|
87
|
+
# Split on &&, ||, ;, and newlines and test a pattern against each segment.
|
|
88
|
+
# Returns 0 if ANY segment matches the pattern.
|
|
89
|
+
any_segment_matches() {
|
|
90
|
+
local PATTERN="$1"
|
|
91
|
+
while IFS= read -r SEG; do
|
|
92
|
+
if printf '%s' "$SEG" | grep -qiE "$PATTERN"; then
|
|
93
|
+
return 0
|
|
94
|
+
fi
|
|
95
|
+
done < <(printf '%s' "$CMD" | sed 's/&&/\n/g; s/||/\n/g; s/;/\n/g')
|
|
96
|
+
return 1
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
# ── 8. Smart exclusion flags ──────────────────────────────────────────────────
|
|
100
|
+
CMD_IS_REBASE_SAFE=0
|
|
101
|
+
if printf '%s' "$CMD" | grep -qiE 'git[[:space:]]+(rebase)[[:space:]].*(--abort|--continue)'; then
|
|
102
|
+
CMD_IS_REBASE_SAFE=1
|
|
103
|
+
fi
|
|
104
|
+
|
|
105
|
+
CMD_IS_CLEAN_DRY=0
|
|
106
|
+
if printf '%s' "$CMD" | grep -qiE 'git[[:space:]]+clean.*([ \t]-n|--dry-run)'; then
|
|
107
|
+
CMD_IS_CLEAN_DRY=1
|
|
108
|
+
fi
|
|
109
|
+
|
|
110
|
+
# ── 9. HIGH severity checks ────────────────────────────────────────────────────
|
|
111
|
+
|
|
112
|
+
# H1: git push --force or -f (per-segment — prevents --force-with-lease poisoning)
|
|
113
|
+
# A segment containing --force-with-lease is excluded; other segments are not.
|
|
114
|
+
while IFS= read -r SEGMENT; do
|
|
115
|
+
SEGMENT=$(printf '%s' "$SEGMENT" | sed 's/^[[:space:]]*//')
|
|
116
|
+
[[ -z "$SEGMENT" ]] && continue
|
|
117
|
+
# Skip segments that use the safe --force-with-lease
|
|
118
|
+
if printf '%s' "$SEGMENT" | grep -qiE 'git[[:space:]]+push.*--force-with-lease'; then
|
|
119
|
+
continue
|
|
120
|
+
fi
|
|
121
|
+
if printf '%s' "$SEGMENT" | grep -qiE 'git[[:space:]]+push.*(--force|-f[[:space:]])' || \
|
|
122
|
+
printf '%s' "$SEGMENT" | grep -qiE 'git[[:space:]]+push.*(--force|-f)$'; then
|
|
123
|
+
add_high \
|
|
124
|
+
"git push --force — force push detected" \
|
|
125
|
+
"Force-pushing rewrites public history and breaks collaborators' local copies." \
|
|
126
|
+
"Alt: Use 'git push --force-with-lease' — blocks if upstream has new commits you haven't pulled."
|
|
127
|
+
break
|
|
128
|
+
fi
|
|
129
|
+
done < <(printf '%s' "$CMD" | sed 's/&&/\n/g; s/||/\n/g; s/;/\n/g')
|
|
130
|
+
|
|
131
|
+
# H2: git rebase — advisory (MEDIUM)
|
|
132
|
+
if [[ $CMD_IS_REBASE_SAFE -eq 0 ]]; then
|
|
133
|
+
if printf '%s' "$CMD" | grep -qiE 'git[[:space:]]+rebase([[:space:]]|$)'; then
|
|
134
|
+
add_medium \
|
|
135
|
+
"git rebase — rewrites commit history (advisory)" \
|
|
136
|
+
"Rebase changes commit SHAs. Safe on local feature branches; dangerous on shared/published branches." \
|
|
137
|
+
"Alt: 'git merge origin/main' preserves history (creates merge commit)." \
|
|
138
|
+
" 'git rebase --abort' to cancel if in progress."
|
|
139
|
+
fi
|
|
140
|
+
fi
|
|
141
|
+
|
|
142
|
+
# H3: git checkout -- .
|
|
143
|
+
if printf '%s' "$CMD" | grep -qiE 'git[[:space:]]+checkout[[:space:]]+--[[:space:]]+\.'; then
|
|
144
|
+
add_high \
|
|
145
|
+
"git checkout -- . — discards all uncommitted changes" \
|
|
146
|
+
"Overwrites working tree changes with HEAD. Uncommitted work is lost permanently." \
|
|
147
|
+
"Alt: 'git stash' to temporarily shelve changes, 'git restore <file>' for individual files."
|
|
148
|
+
fi
|
|
149
|
+
|
|
150
|
+
# H4: git restore . (any form — with or without --staged flag)
|
|
151
|
+
if printf '%s' "$CMD" | grep -qiE 'git[[:space:]]+restore[[:space:]].*[[:space:]]\.([[:space:]]|$)' || \
|
|
152
|
+
printf '%s' "$CMD" | grep -qiE 'git[[:space:]]+restore[[:space:]]+\.[[:space:]]*$'; then
|
|
153
|
+
add_high \
|
|
154
|
+
"git restore . — discards all uncommitted changes" \
|
|
155
|
+
"Restores every tracked file to HEAD, permanently discarding all working tree modifications." \
|
|
156
|
+
"Alt: 'git stash' to save changes temporarily, or restore individual files: 'git restore <file>'."
|
|
157
|
+
fi
|
|
158
|
+
|
|
159
|
+
# H5: git clean -f
|
|
160
|
+
if [[ $CMD_IS_CLEAN_DRY -eq 0 ]]; then
|
|
161
|
+
if printf '%s' "$CMD" | grep -qiE 'git[[:space:]]+clean[[:space:]]+-[a-zA-Z]*f'; then
|
|
162
|
+
add_high \
|
|
163
|
+
"git clean -f — removes untracked files" \
|
|
164
|
+
"Permanently deletes untracked files from the working tree. Cannot be undone via git." \
|
|
165
|
+
"Alt: 'git clean -n' (dry-run) to preview what would be deleted before committing."
|
|
166
|
+
fi
|
|
167
|
+
fi
|
|
168
|
+
|
|
169
|
+
# H6: DROP TABLE or DROP DATABASE in psql
|
|
170
|
+
if printf '%s' "$CMD" | grep -qiE '(psql|pgcli)[^|&;]*DROP[[:space:]]+(TABLE|DATABASE|SCHEMA)'; then
|
|
171
|
+
add_high \
|
|
172
|
+
"DROP TABLE/DATABASE via psql — destructive DDL" \
|
|
173
|
+
"Running destructive DDL directly in psql bypasses migration pipeline safety checks." \
|
|
174
|
+
"Alt: Use your project's migration tool. Never run DROP via ad-hoc psql."
|
|
175
|
+
fi
|
|
176
|
+
|
|
177
|
+
# H7: kill -9 with pgrep subshell
|
|
178
|
+
if printf '%s' "$CMD" | grep -qiE 'kill[[:space:]]+-9[[:space:]]+(\$\(|`)'; then
|
|
179
|
+
add_high \
|
|
180
|
+
"kill -9 with pgrep subshell — aggressive process termination" \
|
|
181
|
+
"Sends SIGKILL to processes matched by name, which may kill unintended processes." \
|
|
182
|
+
"Alt: 'kill -15 <pid>' (SIGTERM) for graceful shutdown."
|
|
183
|
+
fi
|
|
184
|
+
|
|
185
|
+
# H8: killall -9
|
|
186
|
+
if printf '%s' "$CMD" | grep -qiE 'killall[[:space:]]+-9[[:space:]]+\S'; then
|
|
187
|
+
add_high \
|
|
188
|
+
"killall -9 — SIGKILL all matching processes" \
|
|
189
|
+
"Immediately terminates all processes with the given name without cleanup." \
|
|
190
|
+
"Alt: 'killall -15 <name>' (SIGTERM) allows graceful shutdown."
|
|
191
|
+
fi
|
|
192
|
+
|
|
193
|
+
# H9: git commit --no-verify
|
|
194
|
+
if printf '%s' "$CMD" | grep -qiE 'git[[:space:]]+commit.*--no-verify'; then
|
|
195
|
+
add_high \
|
|
196
|
+
"git commit --no-verify — skipping pre-commit hooks" \
|
|
197
|
+
"Bypasses all pre-commit safety gates including secret scanning and linting." \
|
|
198
|
+
"Alt: Fix the underlying hook failure rather than bypassing it."
|
|
199
|
+
fi
|
|
200
|
+
|
|
201
|
+
# H10: HUSKY=0 bypass — suppresses all git hooks without --no-verify
|
|
202
|
+
if printf '%s' "$CMD" | grep -qiE '(^|[[:space:];]|&&|\|\|)HUSKY=0[[:space:]]+git[[:space:]]+(commit|push|tag)'; then
|
|
203
|
+
add_high \
|
|
204
|
+
"HUSKY=0 — bypasses all husky git hooks" \
|
|
205
|
+
"Setting HUSKY=0 disables pre-commit, commit-msg, and pre-push safety gates without --no-verify." \
|
|
206
|
+
"Alt: Fix the underlying hook failure rather than suppressing all hooks."
|
|
207
|
+
fi
|
|
208
|
+
|
|
209
|
+
# H11: rm -rf with broad targets
|
|
210
|
+
# Covers combined flags (rm -rf, rm -fr), split flags (rm -r -f), and long flags (rm --recursive --force)
|
|
211
|
+
BROAD_TARGETS='(\/|~\/|\.\/\*|\*|\.|src|dist|build|node_modules)'
|
|
212
|
+
if printf '%s' "$CMD" | grep -qiE "rm[[:space:]]+-[a-zA-Z]*r[a-zA-Z]*f[[:space:]]+${BROAD_TARGETS}" || \
|
|
213
|
+
printf '%s' "$CMD" | grep -qiE "rm[[:space:]]+-[a-zA-Z]*f[a-zA-Z]*r[[:space:]]+${BROAD_TARGETS}" || \
|
|
214
|
+
printf '%s' "$CMD" | grep -qiE "rm[[:space:]]+-[a-zA-Z]*r[[:space:]]+-[a-zA-Z]*f[[:space:]]+${BROAD_TARGETS}" || \
|
|
215
|
+
printf '%s' "$CMD" | grep -qiE "rm[[:space:]]+-[a-zA-Z]*f[[:space:]]+-[a-zA-Z]*r[[:space:]]+${BROAD_TARGETS}" || \
|
|
216
|
+
printf '%s' "$CMD" | grep -qiE "rm[[:space:]]+--recursive[[:space:]]+--force[[:space:]]+${BROAD_TARGETS}" || \
|
|
217
|
+
printf '%s' "$CMD" | grep -qiE "rm[[:space:]]+--force[[:space:]]+--recursive[[:space:]]+${BROAD_TARGETS}"; then
|
|
218
|
+
add_high \
|
|
219
|
+
"rm -rf with broad target — mass file deletion" \
|
|
220
|
+
"Permanently deletes files and directories. Cannot be undone." \
|
|
221
|
+
"Alt: Move to a temp location first, or use 'rm -ri' for interactive deletion."
|
|
222
|
+
fi
|
|
223
|
+
|
|
224
|
+
# H12: curl/wget piped directly to shell (supply chain attack vector)
|
|
225
|
+
if printf '%s' "$CMD" | grep -qiE '(curl|wget)[^|]*\|[[:space:]]*(bash|sh|zsh|fish)'; then
|
|
226
|
+
add_high \
|
|
227
|
+
"curl/wget piped to shell — remote code execution" \
|
|
228
|
+
"Executing remote scripts without inspection is a major supply chain risk." \
|
|
229
|
+
"Alt: Download first, inspect the script, then execute: curl -o script.sh URL && cat script.sh && bash script.sh"
|
|
230
|
+
fi
|
|
231
|
+
|
|
232
|
+
# ── 10. MEDIUM severity checks ────────────────────────────────────────────────
|
|
233
|
+
|
|
234
|
+
# M1: npm install --force
|
|
235
|
+
if printf '%s' "$CMD" | grep -qiE 'npm[[:space:]]+(install|i)[[:space:]].*--force'; then
|
|
236
|
+
add_medium \
|
|
237
|
+
"npm install --force — bypasses dependency resolution" \
|
|
238
|
+
"--force skips conflict checks and can install incompatible package versions." \
|
|
239
|
+
"Alt: Resolve the dependency conflict explicitly. Use --legacy-peer-deps if needed."
|
|
240
|
+
fi
|
|
241
|
+
|
|
242
|
+
# ── 11. Evaluate and report ───────────────────────────────────────────────────
|
|
243
|
+
|
|
244
|
+
TRUNCATED_CMD=$(truncate_cmd "$CMD")
|
|
245
|
+
|
|
246
|
+
print_violations() {
|
|
247
|
+
local VF="$1"
|
|
248
|
+
local NOTE_LABEL="$2"
|
|
249
|
+
while IFS= read -r LINE; do
|
|
250
|
+
case "$LINE" in
|
|
251
|
+
HIGH\|*|MEDIUM\|*)
|
|
252
|
+
local SEV LABEL DETAIL
|
|
253
|
+
SEV=$(printf '%s' "$LINE" | cut -d'|' -f1)
|
|
254
|
+
LABEL=$(printf '%s' "$LINE" | cut -d'|' -f2)
|
|
255
|
+
DETAIL=$(printf '%s' "$LINE" | cut -d'|' -f3)
|
|
256
|
+
printf ' %s: %s\n' "$SEV" "$LABEL"
|
|
257
|
+
printf ' %s: %s\n' "$NOTE_LABEL" "$DETAIL"
|
|
258
|
+
;;
|
|
259
|
+
ALT:*)
|
|
260
|
+
printf ' %s\n' "${LINE#ALT:}"
|
|
261
|
+
;;
|
|
262
|
+
END_VIOLATION)
|
|
263
|
+
printf '\n'
|
|
264
|
+
;;
|
|
265
|
+
esac
|
|
266
|
+
done < "$VF"
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
if [[ -s "$HIGH_FILE" ]]; then
|
|
270
|
+
{
|
|
271
|
+
printf 'BASH INTERCEPTED: Dangerous command blocked\n'
|
|
272
|
+
print_violations "$HIGH_FILE" "Reason"
|
|
273
|
+
printf ' BLOCKED COMMAND: %s\n' "$TRUNCATED_CMD"
|
|
274
|
+
} >&2
|
|
275
|
+
exit 2
|
|
276
|
+
fi
|
|
277
|
+
|
|
278
|
+
if [[ -s "$MEDIUM_FILE" ]]; then
|
|
279
|
+
{
|
|
280
|
+
printf 'BASH ADVISORY: Potentially risky command (not blocked)\n'
|
|
281
|
+
print_violations "$MEDIUM_FILE" "Note"
|
|
282
|
+
printf ' COMMAND: %s\n' "$TRUNCATED_CMD"
|
|
283
|
+
} >&2
|
|
284
|
+
exit 0
|
|
285
|
+
fi
|
|
286
|
+
|
|
287
|
+
exit 0
|