@jaguilar87/gaia-ops 4.4.0 → 4.5.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/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +12 -3
- package/CHANGELOG.md +34 -0
- package/README.md +43 -11
- package/bin/README.md +2 -2
- package/bin/gaia-doctor.js +18 -5
- package/bin/gaia-scan.py +19 -1
- package/bin/gaia-update.js +208 -40
- package/git-hooks/commit-msg +41 -0
- package/hooks/README.md +5 -4
- package/hooks/modules/README.md +2 -1
- package/hooks/modules/context/compact_context_builder.py +1 -0
- package/hooks/modules/core/hook_entry.py +1 -0
- package/hooks/modules/core/plugin_mode.py +74 -4
- package/hooks/modules/core/plugin_setup.py +286 -21
- package/hooks/modules/identity/ops_identity.py +0 -5
- package/hooks/modules/security/approval_messages.py +9 -19
- package/hooks/modules/security/blocked_commands.py +96 -0
- package/hooks/modules/security/tiers.py +1 -0
- package/hooks/modules/session/session_event_injector.py +1 -0
- package/hooks/modules/tools/bash_validator.py +125 -37
- package/hooks/pre_tool_use.py +4 -14
- package/hooks/session_start.py +4 -2
- package/hooks/user_prompt_submit.py +30 -36
- package/package.json +3 -2
- package/templates/README.md +15 -5
- package/templates/managed-settings.template.json +43 -0
- package/templates/settings.template.json +0 -97
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
{
|
|
9
9
|
"name": "gaia-security",
|
|
10
10
|
"description": "Keeps you in the loop only when it matters. Gaia Security analyzes every command and classifies it into risk tiers: read-only queries run freely, simulations and validations pass through, and state-changing operations (create, delete, apply, push) pause for your explicit approval before executing. Irreversible commands like dropping databases or deleting cloud infrastructure are permanently blocked.",
|
|
11
|
-
"version": "4.
|
|
11
|
+
"version": "4.5.0",
|
|
12
12
|
"source": "./dist/gaia-security"
|
|
13
13
|
}
|
|
14
14
|
]
|
|
@@ -1,15 +1,24 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gaia-ops",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.5.0",
|
|
4
4
|
"description": "Security-first orchestrator with specialized agents, hooks, and governance for AI coding",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "jaguilar87"
|
|
7
7
|
},
|
|
8
8
|
"repository": "https://github.com/metraton/gaia-ops",
|
|
9
9
|
"license": "MIT",
|
|
10
|
-
"keywords": [
|
|
10
|
+
"keywords": [
|
|
11
|
+
"security",
|
|
12
|
+
"devops",
|
|
13
|
+
"orchestrator",
|
|
14
|
+
"governance"
|
|
15
|
+
],
|
|
11
16
|
"engines": {
|
|
12
17
|
"claude-code": ">=2.1.0"
|
|
13
18
|
},
|
|
14
|
-
"categories": [
|
|
19
|
+
"categories": [
|
|
20
|
+
"devops",
|
|
21
|
+
"security",
|
|
22
|
+
"orchestration"
|
|
23
|
+
]
|
|
15
24
|
}
|
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,40 @@ All notable changes to the gaia-ops orchestration system are documented in this
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [4.5.0] - 2026-03-24
|
|
9
|
+
|
|
10
|
+
### Settings Architecture Redesign + Multi-Cloud Security
|
|
11
|
+
|
|
12
|
+
Unified approach for permissions across NPM and plugin installation modes. Permissions now live in `settings.local.json` (union merge, preserves user config). `settings.json` contains only hooks.
|
|
13
|
+
|
|
14
|
+
#### Added
|
|
15
|
+
- **Azure deny rules** — 39 rules covering resource groups, networking, AKS, Key Vault, CosmosDB, Service Bus, and more
|
|
16
|
+
- **Generic wildcard deny rules** — 20 rules that catch all present and future cloud services (`aws * delete-*`, `az * delete`, `gcloud * delete`, etc.)
|
|
17
|
+
- **Indirect execution detection** — Catches `bash -c`, `eval`, `python3 -c`, `node -e`, `ruby -e`, `perl -e` wrappers that bypass regex patterns
|
|
18
|
+
- **Managed settings template** — `templates/managed-settings.template.json` for enterprise deployment via Claude.ai Admin Console
|
|
19
|
+
- **`updateLocalPermissions()`** in `gaia-update.js` — NPM postinstall now merges permissions into `settings.local.json` (same approach as plugin SessionStart)
|
|
20
|
+
- **Plugin mode detection via `plugin.json`** — `plugin_setup.py` and `plugin_mode.py` now read `.claude-plugin/plugin.json` for reliable name/version/mode detection with `--plugin-dir`
|
|
21
|
+
- **First-run welcome message** — `user_prompt_submit.py` detects first run and injects a welcome explaining that restart is needed to activate permissions
|
|
22
|
+
|
|
23
|
+
#### Changed
|
|
24
|
+
- **`settings.template.json`** — Removed permissions block; template now contains only hooks + environment
|
|
25
|
+
- **`_DENY_RULES` centralized in Python** — Single source of truth in `plugin_setup.py`, shared by both OPS and SECURITY modes
|
|
26
|
+
- **T3 approval flow** — All T3 mutative operations now use native `ask` dialog (both ops and security mode). Nonce workflow removed from direct conversation; kept for subagent use via skills.
|
|
27
|
+
- **`approval_messages.py`** — Simplified T3 block message to minimal data (tier + nonce). Workflow instructions live in skills, not hook messages.
|
|
28
|
+
- **`pre_tool_use.py`** — Simplified: passes through `block_response` from `bash_validator` directly, no more mode-specific branching
|
|
29
|
+
- **`bash_validator.py`** — T3 mutative returns `ask` response directly (no nonce generation, no pending files)
|
|
30
|
+
- **`session_start.py`** — Uses `mark_done=False` so `user_prompt_submit.py` can detect first-run and show welcome before marking initialized
|
|
31
|
+
- **`gaia-update.js` registry path** — Fixed to write `plugin-registry.json` in `.claude/` (same path Python hooks expect)
|
|
32
|
+
- **`gaia-doctor.js`** — Now checks permissions in `settings.local.json` (not just `settings.json`). Updated agent and config file lists.
|
|
33
|
+
- **`gaia-update.js` health check** — Updated config files (`surface-routing.json`) and agent list (`gaia-system.md`, `speckit-planner.md`)
|
|
34
|
+
|
|
35
|
+
#### Fixed
|
|
36
|
+
- **Registry path mismatch** — `gaia-update.js` wrote to `.claude/project-context/`, Python read from `.claude/`. Now both use `.claude/`.
|
|
37
|
+
- **Orphaned nonce files** — `bash_validator` no longer writes pending approval files for `ask` responses
|
|
38
|
+
- **Plugin mode detection** — `--plugin-dir` now correctly detects `gaia-ops` vs `gaia-security` via `plugin.json` instead of path parsing
|
|
39
|
+
- **First-run welcome race condition** — `SessionStart` no longer marks initialized; `UserPromptSubmit` marks after showing welcome
|
|
40
|
+
- **`_build_welcome()` framing** — Rewritten to explain WHY the user needs to restart (permissions not active yet), making Claude naturally relay the message
|
|
41
|
+
|
|
8
42
|
## [4.4.0-rc.5] - 2026-03-19
|
|
9
43
|
|
|
10
44
|
### Identity Redesign
|
package/README.md
CHANGED
|
@@ -14,14 +14,17 @@ Multi-agent DevOps system that classifies every operation by risk, routes work t
|
|
|
14
14
|
|
|
15
15
|
### Features
|
|
16
16
|
|
|
17
|
-
- **Multi-cloud support** - GCP, AWS
|
|
18
|
-
- **6 agents** - terraform-architect, gitops-operator, cloud-troubleshooter, devops-developer, speckit-planner, gaia (meta-agent)
|
|
17
|
+
- **Multi-cloud support** - GCP, AWS, Azure
|
|
18
|
+
- **6 agents** - terraform-architect, gitops-operator, cloud-troubleshooter, devops-developer, speckit-planner, gaia-system (meta-agent)
|
|
19
19
|
- **Contracts as SSOT** - Cloud-agnostic base contracts with per-cloud extensions (GCP, AWS)
|
|
20
20
|
- **Dynamic identity** - Orchestrator identity injected by UserPromptSubmit hook; skills loaded on-demand
|
|
21
|
-
- **
|
|
21
|
+
- **Dual-barrier security** - Settings deny rules (Claude Code native) + hook-level blocking (inalterable via symlink)
|
|
22
|
+
- **Indirect execution detection** - Catches `bash -c`, `eval`, `python -c` wrappers that bypass regex patterns
|
|
23
|
+
- **Approval gates** for T3 operations via native `ask` dialog
|
|
22
24
|
- **Git commit validation** with Conventional Commits
|
|
23
|
-
- **
|
|
25
|
+
- **21 skills** - Injected procedural knowledge modules for agents
|
|
24
26
|
- **Plugin + npm** - Distributable as Claude Code native plugin or npm package
|
|
27
|
+
- **Enterprise ready** - Managed settings template for organization-wide deployment
|
|
25
28
|
|
|
26
29
|
## Installation
|
|
27
30
|
|
|
@@ -56,12 +59,24 @@ gaia-scan
|
|
|
56
59
|
|
|
57
60
|
This will:
|
|
58
61
|
1. Auto-detect your project structure (GitOps, Terraform, AppServices)
|
|
59
|
-
2.
|
|
60
|
-
3.
|
|
61
|
-
4.
|
|
62
|
+
2. Create `.claude/` directory with symlinks to this package
|
|
63
|
+
3. Generate `project-context.json`
|
|
64
|
+
4. Create `settings.json` with hooks only (no permissions in settings.json)
|
|
65
|
+
5. Merge deny rules + allow permissions into `settings.local.json` (preserves existing user config)
|
|
62
66
|
|
|
63
67
|
No `CLAUDE.md` is generated -- orchestrator identity is injected dynamically by the UserPromptSubmit hook.
|
|
64
68
|
|
|
69
|
+
### Settings Architecture
|
|
70
|
+
|
|
71
|
+
Gaia-Ops separates hooks from permissions:
|
|
72
|
+
|
|
73
|
+
| File | Content | Strategy |
|
|
74
|
+
|------|---------|----------|
|
|
75
|
+
| `settings.json` | Hooks only (9 hook types) | Overwritten from template on each update |
|
|
76
|
+
| `settings.local.json` | Permissions (allow + deny rules) | Union merge — never removes user config |
|
|
77
|
+
|
|
78
|
+
This ensures your personal customizations (MCP servers, extra permissions) survive updates.
|
|
79
|
+
|
|
65
80
|
### Manual Installation
|
|
66
81
|
|
|
67
82
|
```bash
|
|
@@ -100,17 +115,34 @@ npx gaia-skills-diagnose
|
|
|
100
115
|
npx gaia-skills-diagnose --run-tests
|
|
101
116
|
```
|
|
102
117
|
|
|
118
|
+
## Security
|
|
119
|
+
|
|
120
|
+
Gaia-Ops enforces a 6-layer security pipeline:
|
|
121
|
+
|
|
122
|
+
| Layer | Mechanism | Bypassable? |
|
|
123
|
+
|-------|-----------|-------------|
|
|
124
|
+
| Indirect execution detection | `bash -c`, `eval`, `python -c` wrappers | No (hook-level) |
|
|
125
|
+
| Blocked commands (regex) | 85+ regex patterns | No (symlink to npm package) |
|
|
126
|
+
| Blocked commands (semantic) | 70+ ordered-token rules | No (symlink to npm package) |
|
|
127
|
+
| Cloud pipe validator | Credential piping detection | No (hook-level) |
|
|
128
|
+
| Mutative verb detection | `ask` dialog for state-changing ops | User approves via native dialog |
|
|
129
|
+
| Settings deny rules | 147 deny rules in `settings.local.json` | Self-healing (restored each session) |
|
|
130
|
+
|
|
131
|
+
### Enterprise Deployment
|
|
132
|
+
|
|
133
|
+
For organization-wide enforcement, deploy `templates/managed-settings.template.json` as a managed settings policy via Claude.ai Admin Console. Managed settings have the highest precedence and cannot be overridden.
|
|
134
|
+
|
|
103
135
|
## Project Structure
|
|
104
136
|
|
|
105
137
|
```
|
|
106
138
|
node_modules/@jaguilar87/gaia-ops/
|
|
107
139
|
├── agents/ # Agent definitions (6 agents)
|
|
108
|
-
├── skills/ # Skill modules (
|
|
140
|
+
├── skills/ # Skill modules (21 skills)
|
|
109
141
|
├── tools/ # Orchestration tools
|
|
110
142
|
├── hooks/ # Claude Code hooks (modular architecture)
|
|
111
143
|
├── commands/ # Slash commands (5 speckit + scan-project)
|
|
112
144
|
├── config/ # Configuration (contracts, git standards, rules)
|
|
113
|
-
├── templates/ # Installation templates (settings, governance)
|
|
145
|
+
├── templates/ # Installation templates (settings, governance, managed-settings)
|
|
114
146
|
├── speckit/ # Spec-Kit framework (templates)
|
|
115
147
|
├── bin/ # CLI utilities (11 scripts)
|
|
116
148
|
└── tests/ # Test suite
|
|
@@ -133,7 +165,7 @@ This package follows [Semantic Versioning](https://semver.org/):
|
|
|
133
165
|
- **MINOR:** New features
|
|
134
166
|
- **PATCH:** Bug fixes
|
|
135
167
|
|
|
136
|
-
Current version: **4.
|
|
168
|
+
Current version: **4.5.0**
|
|
137
169
|
|
|
138
170
|
See [CHANGELOG.md](./CHANGELOG.md) for version history.
|
|
139
171
|
|
|
@@ -169,7 +201,7 @@ git clone git@bitbucket.org:yourorg/your-project-context.git project-context
|
|
|
169
201
|
|
|
170
202
|
- **Issues:** [GitHub Issues](https://github.com/metraton/gaia-ops/issues)
|
|
171
203
|
- **Repository:** [github.com/metraton/gaia-ops](https://github.com/metraton/gaia-ops)
|
|
172
|
-
- **Author:** Jorge Aguilar <
|
|
204
|
+
- **Author:** Jorge Aguilar <jorge.aguilar88@gmail.com>
|
|
173
205
|
|
|
174
206
|
## License
|
|
175
207
|
|
package/bin/README.md
CHANGED
|
@@ -27,7 +27,7 @@ Configure symlinks Remove files
|
|
|
27
27
|
| Script | Description |
|
|
28
28
|
|--------|-------------|
|
|
29
29
|
| `gaia-scan` | Project scanner and installer (Python) |
|
|
30
|
-
| `gaia-update.js` | Configuration updater (postinstall hook) |
|
|
30
|
+
| `gaia-update.js` | Configuration updater (postinstall hook) — updates hooks template, merges permissions into settings.local.json, ensures plugin-registry |
|
|
31
31
|
|
|
32
32
|
### Diagnostics and Monitoring
|
|
33
33
|
|
|
@@ -146,4 +146,4 @@ npx gaia-scan --non-interactive
|
|
|
146
146
|
|
|
147
147
|
---
|
|
148
148
|
|
|
149
|
-
**Version:** 4.
|
|
149
|
+
**Version:** 4.5.0 | **Updated:** 2026-03-24 | **Scripts:** 11
|
package/bin/gaia-doctor.js
CHANGED
|
@@ -102,17 +102,30 @@ async function checkSettingsJson() {
|
|
|
102
102
|
if (!hookTypes.includes('PostToolUse')) issues.push('Missing PostToolUse hook');
|
|
103
103
|
}
|
|
104
104
|
|
|
105
|
-
// Check permissions
|
|
106
|
-
|
|
107
|
-
|
|
105
|
+
// Check permissions — now live in settings.local.json (not settings.json)
|
|
106
|
+
const localPath = join(CWD, '.claude', 'settings.local.json');
|
|
107
|
+
let permCount = 0;
|
|
108
|
+
if (existsSync(localPath)) {
|
|
109
|
+
try {
|
|
110
|
+
const localData = JSON.parse(await fs.readFile(localPath, 'utf-8'));
|
|
111
|
+
if (localData.permissions) {
|
|
112
|
+
permCount = Object.values(localData.permissions).flat().length;
|
|
113
|
+
}
|
|
114
|
+
} catch { /* ignore parse errors */ }
|
|
115
|
+
}
|
|
116
|
+
// Also count permissions in settings.json (legacy installs)
|
|
117
|
+
if (data.permissions) {
|
|
118
|
+
permCount += Object.values(data.permissions).flat().length;
|
|
119
|
+
}
|
|
120
|
+
if (permCount === 0) {
|
|
121
|
+
issues.push('No permissions configured (check settings.local.json)');
|
|
108
122
|
}
|
|
109
123
|
|
|
110
124
|
if (issues.length > 0) {
|
|
111
|
-
return { name: 'settings.json', ok: false, detail: issues.join('; '), fix: 'Run gaia-scan' };
|
|
125
|
+
return { name: 'settings.json', ok: false, detail: issues.join('; '), fix: 'Run gaia-scan or npx gaia-update' };
|
|
112
126
|
}
|
|
113
127
|
|
|
114
128
|
const hookCount = data.hooks ? Object.keys(data.hooks).length : 0;
|
|
115
|
-
const permCount = data.permissions ? Object.values(data.permissions).flat().length : 0;
|
|
116
129
|
return { name: 'settings.json', ok: true, detail: `${hookCount} hook types, ${permCount} rules` };
|
|
117
130
|
} catch {
|
|
118
131
|
return { name: 'settings.json', ok: false, detail: 'Invalid JSON', fix: 'Delete and run gaia-scan' };
|
package/bin/gaia-scan.py
CHANGED
|
@@ -238,8 +238,11 @@ def _mode_fresh(project_root: Path, scan_config: ScanConfig, args) -> int:
|
|
|
238
238
|
|
|
239
239
|
# Step 4: INSTALL (automatic, no prompts)
|
|
240
240
|
skip_claude = getattr(args, "skip_claude_install", False)
|
|
241
|
+
npm_postinstall = getattr(args, "npm_postinstall", False)
|
|
241
242
|
ensure_claude_code(skip_install=skip_claude)
|
|
242
|
-
|
|
243
|
+
if not npm_postinstall:
|
|
244
|
+
# Skip when called from npm postinstall to avoid re-entrance
|
|
245
|
+
ensure_gaia_ops_package(project_root)
|
|
243
246
|
create_claude_directory(project_root)
|
|
244
247
|
copy_claude_md(project_root)
|
|
245
248
|
copy_settings_json(project_root)
|
|
@@ -496,6 +499,13 @@ def main(argv: list = None) -> int:
|
|
|
496
499
|
dest="skip_claude_install",
|
|
497
500
|
help="Skip Claude Code CLI installation",
|
|
498
501
|
)
|
|
502
|
+
parser.add_argument(
|
|
503
|
+
"--npm-postinstall",
|
|
504
|
+
action="store_true",
|
|
505
|
+
default=False,
|
|
506
|
+
dest="npm_postinstall",
|
|
507
|
+
help="Called from npm postinstall: skip Claude Code install and npm package install to avoid re-entrance",
|
|
508
|
+
)
|
|
499
509
|
|
|
500
510
|
args = parser.parse_args(argv)
|
|
501
511
|
|
|
@@ -544,11 +554,19 @@ def main(argv: list = None) -> int:
|
|
|
544
554
|
print(json.dumps(result), file=sys.stdout)
|
|
545
555
|
return 0
|
|
546
556
|
|
|
557
|
+
# --npm-postinstall implies --skip-claude-install and skips ensure_gaia_ops_package
|
|
558
|
+
if args.npm_postinstall:
|
|
559
|
+
args.skip_claude_install = True
|
|
560
|
+
|
|
547
561
|
# Mode selection
|
|
548
562
|
# --json or --scan-only: scan-only mode
|
|
549
563
|
if args.json or args.scan_only:
|
|
550
564
|
return _mode_scan_only(project_root, scan_config, args)
|
|
551
565
|
|
|
566
|
+
# --npm-postinstall: fresh install mode with re-entrance protection
|
|
567
|
+
if args.npm_postinstall:
|
|
568
|
+
return _mode_fresh(project_root, scan_config, args)
|
|
569
|
+
|
|
552
570
|
# Detect mode based on .claude/ existence
|
|
553
571
|
claude_dir = project_root / ".claude"
|
|
554
572
|
if claude_dir.is_dir():
|
package/bin/gaia-update.js
CHANGED
|
@@ -7,13 +7,19 @@
|
|
|
7
7
|
* Also available as: npx gaia-update
|
|
8
8
|
*
|
|
9
9
|
* Behavior:
|
|
10
|
-
* - First-time install (.claude/ doesn't exist):
|
|
10
|
+
* - First-time install (.claude/ doesn't exist):
|
|
11
|
+
* 1. Check Python 3 is available
|
|
12
|
+
* 2. Run gaia-scan --npm-postinstall to create .claude/, symlinks, settings, project-context
|
|
13
|
+
* 3. Create plugin-registry.json
|
|
14
|
+
* 4. Merge permissions into settings.local.json
|
|
15
|
+
* 5. Fall through to verification
|
|
11
16
|
* - Update (.claude/ exists):
|
|
12
17
|
* 1. Show version transition (previous → current)
|
|
13
|
-
* 2. settings.json: REPLACE from template (
|
|
14
|
-
* 3.
|
|
15
|
-
* 4.
|
|
16
|
-
* 5.
|
|
18
|
+
* 2. settings.json: REPLACE from template (hooks only — no permissions)
|
|
19
|
+
* 3. Merge permissions into settings.local.json (union, preserves user config)
|
|
20
|
+
* 4. Symlinks: recreate if missing, fix broken ones
|
|
21
|
+
* 5. Verify: hooks, python, project-context, config files
|
|
22
|
+
* 6. Report: summary with any issues found
|
|
17
23
|
*
|
|
18
24
|
* Usage:
|
|
19
25
|
* npm update @jaguilar87/gaia-ops # Automatic via postinstall
|
|
@@ -82,9 +88,9 @@ async function updateSettingsJson() {
|
|
|
82
88
|
return false;
|
|
83
89
|
}
|
|
84
90
|
|
|
85
|
-
// Always replace from template -- template is the source of truth
|
|
91
|
+
// Always replace from template -- template is the source of truth (hooks only)
|
|
86
92
|
await fs.copyFile(templatePath, settingsPath);
|
|
87
|
-
spinner.succeed('settings.json updated from template');
|
|
93
|
+
spinner.succeed('settings.json updated from template (hooks)');
|
|
88
94
|
return true;
|
|
89
95
|
} catch (error) {
|
|
90
96
|
spinner.fail(`settings.json: ${error.message}`);
|
|
@@ -92,6 +98,114 @@ async function updateSettingsJson() {
|
|
|
92
98
|
}
|
|
93
99
|
}
|
|
94
100
|
|
|
101
|
+
async function updateLocalPermissions() {
|
|
102
|
+
const spinner = ora('Merging permissions into settings.local.json...').start();
|
|
103
|
+
try {
|
|
104
|
+
const claudeDir = join(CWD, '.claude');
|
|
105
|
+
const localPath = join(claudeDir, 'settings.local.json');
|
|
106
|
+
|
|
107
|
+
if (!existsSync(claudeDir)) {
|
|
108
|
+
spinner.info('Skipped (.claude/ not found)');
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Load permissions from plugin_setup.py — the single source of truth.
|
|
113
|
+
// We use ast.literal_eval to extract the constants without importing
|
|
114
|
+
// the module (which has relative imports that fail standalone).
|
|
115
|
+
let gaiaPerms;
|
|
116
|
+
try {
|
|
117
|
+
const setupPath = join(__dirname, '..', 'hooks', 'modules', 'core', 'plugin_setup.py');
|
|
118
|
+
const { stdout } = await execAsync(
|
|
119
|
+
`python3 -c "
|
|
120
|
+
import ast, json, re
|
|
121
|
+
|
|
122
|
+
source = open('${setupPath.replace(/'/g, "\\'")}').read()
|
|
123
|
+
|
|
124
|
+
# Extract _DENY_RULES list
|
|
125
|
+
deny_match = re.search(r'^_DENY_RULES\\s*=\\s*\\[', source, re.MULTILINE)
|
|
126
|
+
if deny_match:
|
|
127
|
+
bracket_start = deny_match.start() + source[deny_match.start():].index('[')
|
|
128
|
+
depth, i = 0, bracket_start
|
|
129
|
+
for i, ch in enumerate(source[bracket_start:], bracket_start):
|
|
130
|
+
if ch == '[': depth += 1
|
|
131
|
+
elif ch == ']': depth -= 1
|
|
132
|
+
if depth == 0: break
|
|
133
|
+
deny_rules = ast.literal_eval(source[bracket_start:i+1])
|
|
134
|
+
else:
|
|
135
|
+
deny_rules = []
|
|
136
|
+
|
|
137
|
+
# Extract OPS_PERMISSIONS allow list
|
|
138
|
+
ops_match = re.search(r'^OPS_PERMISSIONS\\s*=', source, re.MULTILINE)
|
|
139
|
+
if ops_match:
|
|
140
|
+
bracket_start = source.index('{', ops_match.start())
|
|
141
|
+
depth, i = 0, bracket_start
|
|
142
|
+
for i, ch in enumerate(source[bracket_start:], bracket_start):
|
|
143
|
+
if ch == '{': depth += 1
|
|
144
|
+
elif ch == '}': depth -= 1
|
|
145
|
+
if depth == 0: break
|
|
146
|
+
# Replace _DENY_RULES reference with actual list for eval
|
|
147
|
+
ops_str = source[bracket_start:i+1].replace('_DENY_RULES', json.dumps(deny_rules))
|
|
148
|
+
ops_perms = ast.literal_eval(ops_str)
|
|
149
|
+
else:
|
|
150
|
+
ops_perms = {'permissions': {'allow': [], 'deny': deny_rules, 'ask': []}}
|
|
151
|
+
|
|
152
|
+
print(json.dumps(ops_perms))
|
|
153
|
+
"`,
|
|
154
|
+
{ timeout: 10000 }
|
|
155
|
+
);
|
|
156
|
+
gaiaPerms = JSON.parse(stdout.trim());
|
|
157
|
+
} catch (pyError) {
|
|
158
|
+
spinner.warn(`Could not load permissions from Python — ${pyError.message || 'unknown error'}`);
|
|
159
|
+
return false;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const ourAllow = new Set(gaiaPerms.permissions.allow || []);
|
|
163
|
+
const ourDeny = new Set(gaiaPerms.permissions.deny || []);
|
|
164
|
+
|
|
165
|
+
// Load existing settings.local.json — preserve everything (enabledPlugins, MCP servers, etc.)
|
|
166
|
+
let existing = {};
|
|
167
|
+
if (existsSync(localPath)) {
|
|
168
|
+
try {
|
|
169
|
+
existing = JSON.parse(await fs.readFile(localPath, 'utf-8'));
|
|
170
|
+
} catch {
|
|
171
|
+
existing = {};
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const perms = existing.permissions || {};
|
|
176
|
+
const currentAllow = new Set(perms.allow || []);
|
|
177
|
+
const currentDeny = new Set(perms.deny || []);
|
|
178
|
+
|
|
179
|
+
// Union merge — add ours without removing user's
|
|
180
|
+
const mergedAllow = [...new Set([...currentAllow, ...ourAllow])].sort();
|
|
181
|
+
const mergedDeny = [...new Set([...currentDeny, ...ourDeny])].sort();
|
|
182
|
+
|
|
183
|
+
// Check if anything changed
|
|
184
|
+
const allowChanged = mergedAllow.length !== currentAllow.size
|
|
185
|
+
|| mergedAllow.some(r => !currentAllow.has(r));
|
|
186
|
+
const denyChanged = mergedDeny.length !== currentDeny.size
|
|
187
|
+
|| mergedDeny.some(r => !currentDeny.has(r));
|
|
188
|
+
|
|
189
|
+
if (!allowChanged && !denyChanged) {
|
|
190
|
+
spinner.succeed('settings.local.json permissions already up to date');
|
|
191
|
+
return false;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Update only permissions, preserve everything else
|
|
195
|
+
existing.permissions = existing.permissions || {};
|
|
196
|
+
existing.permissions.allow = mergedAllow;
|
|
197
|
+
existing.permissions.deny = mergedDeny;
|
|
198
|
+
existing.permissions.ask = existing.permissions.ask || [];
|
|
199
|
+
|
|
200
|
+
await fs.writeFile(localPath, JSON.stringify(existing, null, 2) + '\n');
|
|
201
|
+
spinner.succeed('settings.local.json permissions merged');
|
|
202
|
+
return true;
|
|
203
|
+
} catch (error) {
|
|
204
|
+
spinner.fail(`settings.local.json: ${error.message}`);
|
|
205
|
+
return false;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
95
209
|
async function updateSymlinks() {
|
|
96
210
|
const spinner = ora('Checking symlinks...').start();
|
|
97
211
|
try {
|
|
@@ -206,7 +320,7 @@ async function runVerification() {
|
|
|
206
320
|
}
|
|
207
321
|
|
|
208
322
|
// 4. Config files accessible
|
|
209
|
-
const configFiles = ['
|
|
323
|
+
const configFiles = ['git_standards.json', 'universal-rules.json', 'surface-routing.json'];
|
|
210
324
|
for (const cfg of configFiles) {
|
|
211
325
|
const path = join(CWD, '.claude', 'config', cfg);
|
|
212
326
|
if (existsSync(path)) {
|
|
@@ -218,7 +332,7 @@ async function runVerification() {
|
|
|
218
332
|
}
|
|
219
333
|
|
|
220
334
|
// 5. Agent definitions accessible
|
|
221
|
-
const agentFiles = ['terraform-architect.md', 'gitops-operator.md', 'cloud-troubleshooter.md', 'devops-developer.md', 'gaia.md'];
|
|
335
|
+
const agentFiles = ['terraform-architect.md', 'gitops-operator.md', 'cloud-troubleshooter.md', 'devops-developer.md', 'gaia-system.md', 'speckit-planner.md'];
|
|
222
336
|
let agentsOk = 0;
|
|
223
337
|
for (const agent of agentFiles) {
|
|
224
338
|
if (existsSync(join(CWD, '.claude', 'agents', agent))) agentsOk++;
|
|
@@ -256,48 +370,102 @@ async function runVerification() {
|
|
|
256
370
|
// Main
|
|
257
371
|
// ============================================================================
|
|
258
372
|
|
|
373
|
+
async function runFreshInstall() {
|
|
374
|
+
const packageDir = join(__dirname, '..');
|
|
375
|
+
const scanScript = join(packageDir, 'bin', 'gaia-scan.py');
|
|
376
|
+
const { current } = await detectVersions();
|
|
377
|
+
|
|
378
|
+
console.log(chalk.cyan(`\n gaia-ops ${chalk.green(current)} — fresh install\n`));
|
|
379
|
+
|
|
380
|
+
// 1. Check Python 3 is available
|
|
381
|
+
const spinner = ora('Checking Python 3...').start();
|
|
382
|
+
try {
|
|
383
|
+
await execAsync('python3 --version', { timeout: 5000 });
|
|
384
|
+
spinner.succeed('Python 3 found');
|
|
385
|
+
} catch {
|
|
386
|
+
spinner.warn('Python 3 not found — skipping project setup');
|
|
387
|
+
console.log(chalk.gray(' Install Python 3.9+ and run: npx gaia-scan\n'));
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// 2. Run gaia-scan --npm-postinstall
|
|
392
|
+
const scanSpinner = ora('Running gaia-scan...').start();
|
|
393
|
+
try {
|
|
394
|
+
const { stdout, stderr } = await execAsync(
|
|
395
|
+
`python3 "${scanScript}" --npm-postinstall --root "${CWD}"`,
|
|
396
|
+
{ timeout: 60000 }
|
|
397
|
+
);
|
|
398
|
+
scanSpinner.succeed('Project scanned and configured');
|
|
399
|
+
if (VERBOSE && stdout) console.log(chalk.gray(stdout));
|
|
400
|
+
if (VERBOSE && stderr) console.log(chalk.yellow(stderr));
|
|
401
|
+
} catch (error) {
|
|
402
|
+
scanSpinner.warn('gaia-scan encountered issues (non-fatal)');
|
|
403
|
+
if (VERBOSE && error.stderr) console.log(chalk.gray(error.stderr));
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// 3. Create plugin-registry.json (in .claude/, same path Python hooks expect)
|
|
407
|
+
try {
|
|
408
|
+
const claudeDirPath = join(CWD, '.claude');
|
|
409
|
+
if (!existsSync(claudeDirPath)) {
|
|
410
|
+
await fs.mkdir(claudeDirPath, { recursive: true });
|
|
411
|
+
}
|
|
412
|
+
const registryPath = join(claudeDirPath, 'plugin-registry.json');
|
|
413
|
+
const registry = {
|
|
414
|
+
installed: [{ name: 'gaia-ops', version: current || 'unknown' }],
|
|
415
|
+
source: 'npm-postinstall',
|
|
416
|
+
};
|
|
417
|
+
await fs.writeFile(registryPath, JSON.stringify(registry, null, 2) + '\n');
|
|
418
|
+
} catch {
|
|
419
|
+
// Non-fatal — plugin-registry is a convenience, not critical
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// 4. Merge permissions into settings.local.json (same approach as plugin mode)
|
|
423
|
+
await updateLocalPermissions();
|
|
424
|
+
}
|
|
425
|
+
|
|
259
426
|
async function main() {
|
|
260
427
|
const claudeDir = join(CWD, '.claude');
|
|
261
428
|
const isUpdate = existsSync(claudeDir);
|
|
262
429
|
|
|
263
430
|
if (!isUpdate) {
|
|
264
|
-
// First-time install — gaia-scan
|
|
265
|
-
|
|
431
|
+
// First-time install — run gaia-scan to bootstrap everything
|
|
432
|
+
await runFreshInstall();
|
|
433
|
+
} else {
|
|
434
|
+
// Version info
|
|
435
|
+
const { previous, current } = await detectVersions();
|
|
436
|
+
const versionLine = previous && previous !== current
|
|
437
|
+
? `${chalk.gray(previous)} → ${chalk.green(current)}`
|
|
438
|
+
: chalk.green(current);
|
|
439
|
+
|
|
440
|
+
console.log(chalk.cyan(`\n gaia-ops update ${versionLine}\n`));
|
|
441
|
+
|
|
442
|
+
// Step 1-3: Update files
|
|
443
|
+
await updateSettingsJson();
|
|
444
|
+
await updateLocalPermissions();
|
|
445
|
+
await updateSymlinks();
|
|
266
446
|
}
|
|
267
447
|
|
|
268
|
-
//
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
448
|
+
// Ensure plugin-registry.json exists in .claude/ (both fresh and update)
|
|
449
|
+
try {
|
|
450
|
+
const registryPath = join(CWD, '.claude', 'plugin-registry.json');
|
|
451
|
+
if (!existsSync(registryPath)) {
|
|
452
|
+
const { current } = await detectVersions();
|
|
453
|
+
const registry = {
|
|
454
|
+
installed: [{ name: 'gaia-ops', version: current || 'unknown' }],
|
|
455
|
+
source: 'npm-postinstall',
|
|
456
|
+
};
|
|
457
|
+
await fs.writeFile(registryPath, JSON.stringify(registry, null, 2) + '\n');
|
|
458
|
+
}
|
|
459
|
+
} catch { /* non-fatal */ }
|
|
279
460
|
|
|
280
|
-
//
|
|
461
|
+
// Verify (runs for both fresh install and update)
|
|
281
462
|
const { issues, passed, total } = await runVerification();
|
|
282
463
|
|
|
283
|
-
// Summary
|
|
284
|
-
const changes = [settingsUpdated, symlinksUpdated].filter(Boolean).length;
|
|
285
|
-
|
|
286
464
|
console.log('');
|
|
287
|
-
if (
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
console.log(chalk.
|
|
291
|
-
if (settingsUpdated) console.log(chalk.gray(' settings.json: replaced from template'));
|
|
292
|
-
if (symlinksFix > 0) console.log(chalk.gray(` ${symlinksFix} symlink(s) fixed`));
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
// Issues
|
|
296
|
-
if (issues.length > 0) {
|
|
297
|
-
console.log(chalk.yellow(`\n ${issues.length} issue(s) found:`));
|
|
298
|
-
for (const issue of issues) {
|
|
299
|
-
console.log(chalk.yellow(` - ${issue}`));
|
|
300
|
-
}
|
|
465
|
+
if (issues.length > 0) {
|
|
466
|
+
console.log(chalk.yellow(` ${issues.length} issue(s) found:`));
|
|
467
|
+
for (const issue of issues) {
|
|
468
|
+
console.log(chalk.yellow(` - ${issue}`));
|
|
301
469
|
}
|
|
302
470
|
} else {
|
|
303
471
|
console.log(chalk.green(' Everything up to date'));
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
#
|
|
3
|
+
# commit-msg hook: strip Claude Code attribution footers from commit messages.
|
|
4
|
+
#
|
|
5
|
+
# This hook runs at the git level, catching ALL commits regardless of whether
|
|
6
|
+
# they originate from Claude Code's Bash tool, a subagent, or the user's terminal.
|
|
7
|
+
#
|
|
8
|
+
# Patterns match those in hooks/modules/tools/bash_validator.py _strip_claude_footers()
|
|
9
|
+
# to maintain a single source of truth for what constitutes a forbidden footer.
|
|
10
|
+
#
|
|
11
|
+
# Installation:
|
|
12
|
+
# cp git-hooks/commit-msg .git/hooks/commit-msg
|
|
13
|
+
# chmod +x .git/hooks/commit-msg
|
|
14
|
+
#
|
|
15
|
+
# Or via gaia-init (automatic).
|
|
16
|
+
|
|
17
|
+
COMMIT_MSG_FILE="$1"
|
|
18
|
+
|
|
19
|
+
if [ ! -f "${COMMIT_MSG_FILE}" ]; then
|
|
20
|
+
exit 0
|
|
21
|
+
fi
|
|
22
|
+
|
|
23
|
+
# Create a temp file for the cleaned message
|
|
24
|
+
TEMP_FILE=$(mktemp)
|
|
25
|
+
trap 'rm -f "${TEMP_FILE}"' EXIT
|
|
26
|
+
|
|
27
|
+
# Read the commit message and strip forbidden footer lines:
|
|
28
|
+
# - Co-Authored-By: containing "Claude" (any case)
|
|
29
|
+
# - "Generated with Claude Code" or "[Claude Code]" (any case)
|
|
30
|
+
# - Emoji-prefixed "Generated with" lines
|
|
31
|
+
sed -E \
|
|
32
|
+
-e '/^[[:space:]]*[Cc][Oo]-[Aa][Uu][Tt][Hh][Oo][Rr][Ee][Dd]-[Bb][Yy]:.*[Cc][Ll][Aa][Uu][Dd][Ee]/d' \
|
|
33
|
+
-e '/^[[:space:]]*[Gg][Ee][Nn][Ee][Rr][Aa][Tt][Ee][Dd] [Ww][Ii][Tt][Hh].*[Cc][Ll][Aa][Uu][Dd][Ee] [Cc][Oo][Dd][Ee]/d' \
|
|
34
|
+
-e '/^[[:space:]]*..?[[:space:]]*[Gg][Ee][Nn][Ee][Rr][Aa][Tt][Ee][Dd] [Ww][Ii][Tt][Hh]/d' \
|
|
35
|
+
"${COMMIT_MSG_FILE}" > "${TEMP_FILE}"
|
|
36
|
+
|
|
37
|
+
# Remove trailing blank lines (collapse to single trailing newline)
|
|
38
|
+
# This prevents empty trailers after stripping
|
|
39
|
+
sed -e :a -e '/^\n*$/{$d;N;ba' -e '}' "${TEMP_FILE}" > "${COMMIT_MSG_FILE}"
|
|
40
|
+
|
|
41
|
+
exit 0
|
package/hooks/README.md
CHANGED
|
@@ -49,7 +49,8 @@ Command executes
|
|
|
49
49
|
| **T0** | Read-only (get, list) | No | pre_tool_use |
|
|
50
50
|
| **T1** | Local validation (validate, lint) | No | pre_tool_use |
|
|
51
51
|
| **T2** | Simulation (plan, diff) | No | pre_tool_use |
|
|
52
|
-
| **T3** | Execution (apply, delete) | **Yes** | pre_tool_use |
|
|
52
|
+
| **T3** | Execution (apply, delete) | **Yes** (native `ask` dialog) | pre_tool_use |
|
|
53
|
+
| **T3-blocked** | Irreversible (delete-vpc, drop db) | **Permanently blocked** | pre_tool_use (exit 2) |
|
|
53
54
|
|
|
54
55
|
## File Structure
|
|
55
56
|
|
|
@@ -70,6 +71,6 @@ hooks/
|
|
|
70
71
|
|
|
71
72
|
---
|
|
72
73
|
|
|
73
|
-
**Version:** 4.
|
|
74
|
-
**Last updated:** 2026-03-
|
|
75
|
-
**Total hooks:**
|
|
74
|
+
**Version:** 4.5.0
|
|
75
|
+
**Last updated:** 2026-03-24
|
|
76
|
+
**Total hooks:** 9 hook scripts (4 primary + 4 event handlers + post_compact)
|
package/hooks/modules/README.md
CHANGED
|
@@ -173,11 +173,12 @@ All security rules (blocked patterns, mutative verbs, tiers) are hardcoded in th
|
|
|
173
173
|
|
|
174
174
|
### Validation Order (Defense-in-Depth)
|
|
175
175
|
bash_validator checks commands in this order (short-circuit on first match):
|
|
176
|
+
0. **Indirect execution detection** — `bash -c`, `eval`, `python -c` etc. → ask or block
|
|
176
177
|
1. **Blocked commands** (blocked_commands.py) — permanently denied patterns, exit 2
|
|
177
178
|
2. **Claude footer stripping** — transparent via updatedInput
|
|
178
179
|
3. **Commit message validation** — conventional commits enforcement
|
|
179
180
|
4. **Cloud pipe/redirect/chain check** (cloud_pipe_validator.py) — corrective deny
|
|
180
|
-
5. **Mutative verbs** (mutative_verbs.py) — CLI-agnostic verb detector,
|
|
181
|
+
5. **Mutative verbs** (mutative_verbs.py) — CLI-agnostic verb detector, native `ask` dialog
|
|
181
182
|
6. **GitOps validation** (gitops_validator.py) — kubectl/helm/flux policy enforcement
|
|
182
183
|
7. **Everything else** — SAFE by elimination (auto-approved)
|
|
183
184
|
|