@sugar-crash-studios/vibe-forge 0.4.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/commands/clear-attention.md +63 -0
- package/.claude/commands/compact-context.md +52 -0
- package/.claude/commands/configure-vcs.md +102 -0
- package/.claude/commands/forge.md +171 -0
- package/.claude/commands/need-help.md +77 -0
- package/.claude/commands/update-status.md +64 -0
- package/.claude/commands/worker-loop.md +106 -0
- package/.claude/hooks/worker-loop.js +198 -0
- package/.claude/scripts/setup-worker-loop.sh +45 -0
- package/.claude/settings.local.json +46 -0
- package/LICENSE +21 -0
- package/README.md +238 -0
- package/agents/aegis/personality.md +294 -0
- package/agents/anvil/personality.md +276 -0
- package/agents/architect/personality.md +258 -0
- package/agents/crucible/personality.md +360 -0
- package/agents/ember/personality.md +291 -0
- package/agents/forge-master/capabilities.md +144 -0
- package/agents/forge-master/context-template.md +128 -0
- package/agents/forge-master/personality.md +138 -0
- package/agents/furnace/personality.md +340 -0
- package/agents/herald/personality.md +247 -0
- package/agents/loki/personality.md +108 -0
- package/agents/oracle/personality.md +283 -0
- package/agents/pixel/personality.md +113 -0
- package/agents/planning-hub/personality.md +320 -0
- package/agents/scribe/personality.md +251 -0
- package/agents/temper/personality.md +218 -0
- package/bin/cli.js +375 -0
- package/bin/dashboard/api/agents.js +333 -0
- package/bin/dashboard/api/dispatch.js +483 -0
- package/bin/dashboard/api/tasks.js +416 -0
- package/bin/dashboard/frontend/index.html +13 -0
- package/bin/dashboard/frontend/package.json +16 -0
- package/bin/dashboard/frontend/src/App.svelte +222 -0
- package/bin/dashboard/frontend/src/app.css +1777 -0
- package/bin/dashboard/frontend/src/lib/components/AgentCard.svelte +60 -0
- package/bin/dashboard/frontend/src/lib/components/AgentsPanel.svelte +57 -0
- package/bin/dashboard/frontend/src/lib/components/DispatchModal.svelte +180 -0
- package/bin/dashboard/frontend/src/lib/components/Footer.svelte +33 -0
- package/bin/dashboard/frontend/src/lib/components/Header.svelte +84 -0
- package/bin/dashboard/frontend/src/lib/components/IssueCard.svelte +33 -0
- package/bin/dashboard/frontend/src/lib/components/IssuesPanel.svelte +73 -0
- package/bin/dashboard/frontend/src/lib/components/KeyboardShortcutsModal.svelte +108 -0
- package/bin/dashboard/frontend/src/lib/components/MobileTabs.svelte +52 -0
- package/bin/dashboard/frontend/src/lib/components/NotificationCard.svelte +60 -0
- package/bin/dashboard/frontend/src/lib/components/NotificationsPanel.svelte +44 -0
- package/bin/dashboard/frontend/src/lib/components/TaskCard.svelte +63 -0
- package/bin/dashboard/frontend/src/lib/components/TasksPanel.svelte +82 -0
- package/bin/dashboard/frontend/src/lib/components/Toast.svelte +45 -0
- package/bin/dashboard/frontend/src/lib/stores/agents.js +34 -0
- package/bin/dashboard/frontend/src/lib/stores/issues.js +54 -0
- package/bin/dashboard/frontend/src/lib/stores/notifications.js +48 -0
- package/bin/dashboard/frontend/src/lib/stores/tasks.js +63 -0
- package/bin/dashboard/frontend/src/lib/stores/theme.js +33 -0
- package/bin/dashboard/frontend/src/lib/stores/toast.js +35 -0
- package/bin/dashboard/frontend/src/lib/stores/ui.js +25 -0
- package/bin/dashboard/frontend/src/lib/stores/voice.js +275 -0
- package/bin/dashboard/frontend/src/lib/stores/websocket.js +295 -0
- package/bin/dashboard/frontend/src/lib/utils/api.js +101 -0
- package/bin/dashboard/frontend/src/lib/utils/formatters.js +54 -0
- package/bin/dashboard/frontend/src/main.js +9 -0
- package/bin/dashboard/frontend/svelte.config.js +5 -0
- package/bin/dashboard/frontend/vite.config.js +20 -0
- package/bin/dashboard/public/assets/index-DnfVj9Ce.css +1 -0
- package/bin/dashboard/public/assets/index-Ze5h0kXQ.js +2 -0
- package/bin/dashboard/public/index.html +14 -0
- package/bin/dashboard/server.js +566 -0
- package/bin/forge-daemon.sh +463 -0
- package/bin/forge-setup.sh +645 -0
- package/bin/forge-spawn.sh +164 -0
- package/bin/forge.cmd +83 -0
- package/bin/forge.sh +533 -0
- package/bin/lib/agents.sh +177 -0
- package/bin/lib/colors.sh +44 -0
- package/bin/lib/config.sh +347 -0
- package/bin/lib/constants.sh +241 -0
- package/bin/lib/daemon/display.sh +128 -0
- package/bin/lib/daemon/notifications.sh +263 -0
- package/bin/lib/daemon/routing.sh +77 -0
- package/bin/lib/daemon/state.sh +115 -0
- package/bin/lib/daemon/sync.sh +95 -0
- package/bin/lib/database.sh +310 -0
- package/bin/lib/heimdall-setup.js +113 -0
- package/bin/lib/heimdall.js +265 -0
- package/bin/lib/json.sh +264 -0
- package/bin/lib/terminal.js +451 -0
- package/bin/lib/util.sh +126 -0
- package/bin/lib/vcs.js +349 -0
- package/config/agent-manifest.yaml +203 -0
- package/config/agents.json +168 -0
- package/config/task-template.md +159 -0
- package/config/task-types.yaml +106 -0
- package/context/agent-status/aegis.json +7 -0
- package/context/agent-status/anvil.json +7 -0
- package/context/agent-status/architect.json +7 -0
- package/context/agent-status/crucible.json +7 -0
- package/context/agent-status/ember.json +7 -0
- package/context/agent-status/furnace.json +7 -0
- package/context/agent-status/loki.json +7 -0
- package/context/agent-status/oracle.json +7 -0
- package/context/agent-status/pixel.json +7 -0
- package/context/agent-status/planning-hub.json +7 -0
- package/context/agent-status/scribe.json +7 -0
- package/context/agent-status/temper.json +7 -0
- package/context/feature-brainstorm.md +426 -0
- package/context/forge-state.yaml +19 -0
- package/context/modern-conventions.md +129 -0
- package/context/project-context-template.md +122 -0
- package/context/project-context.md +122 -0
- package/docs/TODO.md +150 -0
- package/docs/agents.md +409 -0
- package/docs/architecture/decisions/ADR-001-daemon-modularization.md +122 -0
- package/docs/architecture/vibe-lab-integration.md +684 -0
- package/docs/architecture.md +194 -0
- package/docs/bmad-gap-analysis-2026-03-31.md +444 -0
- package/docs/cleanup-workflow.md +329 -0
- package/docs/commands.md +451 -0
- package/docs/dashboard-mockup.html +989 -0
- package/docs/getting-started.md +261 -0
- package/docs/integration/forge-ownership-policy.md +112 -0
- package/docs/npm-publishing.md +132 -0
- package/docs/roadmap-2026.md +519 -0
- package/docs/security.md +144 -0
- package/docs/wireframes/dashboard-mvp.md +1164 -0
- package/docs/workflows/README.md +32 -0
- package/docs/workflows/azure-devops.md +108 -0
- package/docs/workflows/bitbucket.md +104 -0
- package/docs/workflows/git-only.md +130 -0
- package/docs/workflows/gitea.md +168 -0
- package/docs/workflows/github.md +103 -0
- package/docs/workflows/gitlab.md +105 -0
- package/docs/workflows.md +454 -0
- package/package.json +73 -0
- package/tasks/completed/ARCH-001-duplicate-agent-config.md +121 -0
- package/tasks/completed/ARCH-002-mixed-bash-node-implementation.md +88 -0
- package/tasks/completed/ARCH-003-worker-loop-hook-duplication.md +77 -0
- package/tasks/completed/ARCH-009-test-organization.md +78 -0
- package/tasks/completed/ARCH-011-jq-vs-nodejs-json.md +94 -0
- package/tasks/completed/ARCH-012-tmp-files-in-root.md +71 -0
- package/tasks/completed/ARCH-013-exit-code-constants.md +65 -0
- package/tasks/completed/ARCH-014-sed-incompatibility.md +96 -0
- package/tasks/completed/ARCH-015-docs-todo-tracking.md +83 -0
- package/tasks/completed/BUG-dash-001-tasks-filter-error.md +31 -0
- package/tasks/completed/BUG-dash-002-agents-unknown.md +41 -0
- package/tasks/completed/CLEAN-001.md +38 -0
- package/tasks/completed/CLEAN-002.md +43 -0
- package/tasks/completed/CLEAN-003.md +47 -0
- package/tasks/completed/CLEAN-004.md +56 -0
- package/tasks/completed/CLEAN-005.md +75 -0
- package/tasks/completed/CLEAN-006.md +47 -0
- package/tasks/completed/CLEAN-007.md +34 -0
- package/tasks/completed/CLEAN-008.md +49 -0
- package/tasks/completed/CLEAN-012.md +58 -0
- package/tasks/completed/CLEAN-013.md +45 -0
- package/tasks/completed/FEATURE-001a-dashboard-wireframes.md +162 -0
- package/tasks/completed/IMPL-007a-daemon-notifications-module.md +82 -0
- package/tasks/completed/IMPL-007b-daemon-sync-module.md +71 -0
- package/tasks/completed/IMPL-007c-daemon-state-module.md +80 -0
- package/tasks/completed/IMPL-007d-daemon-routing-module.md +77 -0
- package/tasks/completed/IMPL-007e-daemon-display-module.md +77 -0
- package/tasks/completed/IMPL-007f-daemon-integration.md +124 -0
- package/tasks/completed/PLAT-1-heimdall.md +420 -0
- package/tasks/completed/SEC-001-sql-injection-fix.md +58 -0
- package/tasks/completed/SEC-002-notification-injection-fix.md +45 -0
- package/tasks/completed/SEC-003-eval-injection-fix.md +54 -0
- package/tasks/completed/SEC-004-pid-race-condition-fix.md +49 -0
- package/tasks/completed/SEC-005-worker-loop-path-fix.md +51 -0
- package/tasks/completed/SEC-006-eval-agent-names.md +55 -0
- package/tasks/completed/SEC-007-spawn-escaping.md +67 -0
- package/tasks/completed/TASK-DASH-001-server-infrastructure.md +185 -0
- package/tasks/completed/TASK-anvil-001-dashboard-frontend.md +133 -0
- package/tasks/completed/review-bmad-aegis.md +89 -0
- package/tasks/completed/review-bmad-anvil.md +80 -0
- package/tasks/completed/review-bmad-crucible.md +81 -0
- package/tasks/completed/review-bmad-ember.md +90 -0
- package/tasks/completed/review-bmad-furnace.md +79 -0
- package/tasks/completed/review-bmad-pixel.md +82 -0
- package/tasks/completed/review-bmad-scribe.md +92 -0
- package/tasks/completed/review-bmad-sentinel.md +83 -0
- package/tasks/pending/ARCH-004-git-bash-detection-duplication.md +72 -0
- package/tasks/pending/ARCH-005-missing-src-directory.md +95 -0
- package/tasks/pending/ARCH-006-task-template-location.md +64 -0
- package/tasks/pending/ARCH-008-forge-master-vs-hub.md +81 -0
- package/tasks/pending/ARCH-010-missing-index-files.md +84 -0
- package/tasks/pending/CLEAN-009.md +31 -0
- package/tasks/pending/CLEAN-010.md +30 -0
- package/tasks/pending/CLEAN-011.md +30 -0
- package/tasks/pending/CLEAN-014.md +32 -0
- package/tasks/pending/DESIGN-dash-001-layout-review.md +45 -0
- package/tasks/pending/FEATURE-001-dashboard-mvp.md +268 -0
- package/tasks/review/ARCH-007-daemon-monolith.md +162 -0
- package/tasks/review/bmad-review-aegis.md +349 -0
- package/tasks/review/bmad-review-anvil.md +259 -0
- package/tasks/review/bmad-review-crucible.md +277 -0
- package/tasks/review/bmad-review-ember.md +307 -0
- package/tasks/review/bmad-review-furnace.md +285 -0
- package/tasks/review/bmad-review-pixel.md +329 -0
- package/tasks/review/bmad-review-scribe.md +361 -0
- package/tasks/review/bmad-review-sentinel.md +242 -0
- package/tasks/review/task-001.md +78 -0
|
@@ -0,0 +1,420 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: PLAT-1
|
|
3
|
+
title: "Heimdall -- forge worker pre-tool hook interceptor"
|
|
4
|
+
type: security
|
|
5
|
+
priority: high
|
|
6
|
+
status: pending
|
|
7
|
+
assigned_to: null
|
|
8
|
+
epic: PLAT
|
|
9
|
+
estimated_complexity: medium
|
|
10
|
+
depends_on: []
|
|
11
|
+
blocked_by: []
|
|
12
|
+
created: 2026-04-01T00:00:00-05:00
|
|
13
|
+
lab_story_id: null
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
# Context
|
|
17
|
+
|
|
18
|
+
## Why This Exists
|
|
19
|
+
|
|
20
|
+
Forge workers running in background daemon mode use `--dangerously-skip-permissions`.
|
|
21
|
+
In terminal tab mode, a human can observe and intervene. In unattended background mode
|
|
22
|
+
there is no runtime human gate.
|
|
23
|
+
|
|
24
|
+
Heimdall is the compensating control. It intercepts every tool call before it executes,
|
|
25
|
+
checks it against per-task policy, and blocks or allows with an audit log entry.
|
|
26
|
+
|
|
27
|
+
Named for the Norse watchman of the gods. The Gjallarhorn escalation is the signal that
|
|
28
|
+
something has gone wrong and a human must intervene.
|
|
29
|
+
|
|
30
|
+
## Architecture Reference
|
|
31
|
+
|
|
32
|
+
`docs/architecture/vibe-lab-integration.md` -- Security Model section
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
# Acceptance Criteria
|
|
37
|
+
|
|
38
|
+
1. Heimdall is registered as a `PreToolUse` hook in `.claude/settings.local.json`
|
|
39
|
+
written by the forge daemon to each worker's working directory at startup
|
|
40
|
+
2. Every hard-block category listed below returns exit `2` with a clear message
|
|
41
|
+
3. Every audit-log category is allowed (exit `0`) and appended to the daemon log
|
|
42
|
+
4. Context file (`.context.json`) is read on every invocation -- policy is
|
|
43
|
+
per-task, not global
|
|
44
|
+
5. After N violations in a single task (default 3, configurable), Heimdall writes
|
|
45
|
+
`{story-id}.escalation` signal file and logs `HEIMDALL SOUNDED`
|
|
46
|
+
6. Forge daemon detects `.escalation` file and writes escalation handoff
|
|
47
|
+
7. All existing forge tests pass -- no regression on forge-native tasks
|
|
48
|
+
8. Heimdall exits `0` immediately when no context file is present (forge-native
|
|
49
|
+
tasks without a `.context.json` are not restricted by Heimdall)
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
# Tasks / Subtasks
|
|
54
|
+
|
|
55
|
+
## 1. `bin/lib/heimdall.js` -- core interceptor
|
|
56
|
+
|
|
57
|
+
```javascript
|
|
58
|
+
#!/usr/bin/env node
|
|
59
|
+
// Heimdall -- PreToolUse hook for forge workers running in lab pipeline
|
|
60
|
+
// Reads tool call from stdin, checks policy, exits 0 (allow) or 2 (block)
|
|
61
|
+
|
|
62
|
+
import { readFileSync, existsSync, appendFileSync, writeFileSync } from 'fs'
|
|
63
|
+
import { join, resolve } from 'path'
|
|
64
|
+
|
|
65
|
+
const input = JSON.parse(readFileSync('/dev/stdin', 'utf8'))
|
|
66
|
+
const { tool_name, tool_input } = input
|
|
67
|
+
|
|
68
|
+
// Locate context file -- written by forge daemon alongside the inbox task
|
|
69
|
+
// Convention: .context.json in the working directory
|
|
70
|
+
const contextPath = join(process.cwd(), '.context.json')
|
|
71
|
+
|
|
72
|
+
// No context file = forge-native task, not a lab pipeline task
|
|
73
|
+
// Heimdall does not restrict forge-native work
|
|
74
|
+
if (!existsSync(contextPath)) {
|
|
75
|
+
process.exit(0)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const ctx = JSON.parse(readFileSync(contextPath, 'utf8'))
|
|
79
|
+
const { story_id, agent, worktree_path, handoff_dir, has_db_migration, allowed_paths } = ctx
|
|
80
|
+
|
|
81
|
+
const AUDIT_LOG = join(process.env.FORGE_DAEMON_LOG || process.cwd(), 'heimdall-audit.log')
|
|
82
|
+
const VIOLATION_FILE = join(process.cwd(), `${story_id}.violations`)
|
|
83
|
+
const MAX_VIOLATIONS = parseInt(process.env.HEIMDALL_MAX_VIOLATIONS || '3', 10)
|
|
84
|
+
|
|
85
|
+
function timestamp() {
|
|
86
|
+
return new Date().toISOString()
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function audit(level, message) {
|
|
90
|
+
const line = `[${timestamp()}] HEIMDALL ${level} ${agent}/${story_id}: ${message}\n`
|
|
91
|
+
appendFileSync(AUDIT_LOG, line)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function block(reason) {
|
|
95
|
+
audit('BLOCKED', reason)
|
|
96
|
+
|
|
97
|
+
// Track violation count
|
|
98
|
+
let violations = 0
|
|
99
|
+
if (existsSync(VIOLATION_FILE)) {
|
|
100
|
+
violations = parseInt(readFileSync(VIOLATION_FILE, 'utf8').trim(), 10) || 0
|
|
101
|
+
}
|
|
102
|
+
violations++
|
|
103
|
+
writeFileSync(VIOLATION_FILE, String(violations))
|
|
104
|
+
|
|
105
|
+
if (violations >= MAX_VIOLATIONS) {
|
|
106
|
+
const escalationFile = join(process.cwd(), `${story_id}.escalation`)
|
|
107
|
+
writeFileSync(escalationFile, JSON.stringify({
|
|
108
|
+
story_id, agent, violations, reason, timestamp: timestamp()
|
|
109
|
+
}))
|
|
110
|
+
audit('SOUNDED', `${violations} violations, escalating to human review`)
|
|
111
|
+
console.log(
|
|
112
|
+
`[HEIMDALL] Action blocked: ${reason}\n` +
|
|
113
|
+
`[HEIMDALL] GJALLARHORN SOUNDED: ${violations} violations. Story escalated to human review.`
|
|
114
|
+
)
|
|
115
|
+
} else {
|
|
116
|
+
console.log(
|
|
117
|
+
`[HEIMDALL] Action blocked: ${reason}\n` +
|
|
118
|
+
`[HEIMDALL] Violation ${violations}/${MAX_VIOLATIONS}. Self-correct and continue.`
|
|
119
|
+
)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
process.exit(2)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function allow(note) {
|
|
126
|
+
if (note) audit('ALLOWED', note)
|
|
127
|
+
process.exit(0)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function isOutsideAllowedPaths(targetPath) {
|
|
131
|
+
const resolved = resolve(targetPath)
|
|
132
|
+
return !allowed_paths.some(p => resolved.startsWith(resolve(p)))
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// ── BASH tool checks ────────────────────────────────────────────────────────
|
|
136
|
+
if (tool_name === 'Bash') {
|
|
137
|
+
const cmd = (tool_input.command || '').trim()
|
|
138
|
+
|
|
139
|
+
// Catastrophic filesystem operations
|
|
140
|
+
if (/rm\s+-[a-zA-Z]*r[a-zA-Z]*f|rm\s+-[a-zA-Z]*f[a-zA-Z]*r/.test(cmd)) {
|
|
141
|
+
// rm -rf or rm -fr -- check if target is outside worktree
|
|
142
|
+
const rmTarget = cmd.replace(/rm\s+-\S+\s+/, '').trim()
|
|
143
|
+
if (!rmTarget || isOutsideAllowedPaths(rmTarget) ||
|
|
144
|
+
/^(\/|~|\.\.)/.test(rmTarget.replace(/^['"]|['"]$/g, ''))) {
|
|
145
|
+
block(`destructive rm targeting outside worktree: ${cmd}`)
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Credential access
|
|
150
|
+
if (/\.(env|pem|key|cert)\b/.test(cmd) ||
|
|
151
|
+
/~\/\.(ssh|aws)/.test(cmd) ||
|
|
152
|
+
/TOKEN|SECRET|PASSWORD|API_KEY/i.test(cmd) && /echo|print|cat|export\s+\w+=/.test(cmd)) {
|
|
153
|
+
block(`credential access attempt: ${cmd}`)
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Dangerous git operations
|
|
157
|
+
if (/git\s+push\s+.*--force/.test(cmd)) block(`force push blocked: ${cmd}`)
|
|
158
|
+
if (/git\s+reset\s+--hard\s+HEAD~[2-9]/.test(cmd)) block(`multi-commit reset blocked: ${cmd}`)
|
|
159
|
+
if (/git\s+clean\s+-[a-zA-Z]*f[a-zA-Z]*d/.test(cmd)) block(`git clean -fd blocked: ${cmd}`)
|
|
160
|
+
if (/git\s+push\s+(?!origin\b)/.test(cmd)) block(`push to non-origin remote blocked: ${cmd}`)
|
|
161
|
+
|
|
162
|
+
// DB operations without authorization
|
|
163
|
+
if (/(DROP\s+TABLE|TRUNCATE\s+TABLE)/i.test(cmd) && !has_db_migration) {
|
|
164
|
+
block(`DROP/TRUNCATE requires has_db_migration: true in story submission`)
|
|
165
|
+
}
|
|
166
|
+
if (/DELETE\s+FROM\s+\w+\s*;/i.test(cmd)) {
|
|
167
|
+
block(`DELETE without WHERE clause blocked: ${cmd}`)
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// High-risk but legitimate -- audit log only
|
|
171
|
+
if (/npm\s+install|pip\s+install|cargo\s+build|yarn\s+add/.test(cmd)) {
|
|
172
|
+
allow(`dependency install: ${cmd}`)
|
|
173
|
+
}
|
|
174
|
+
if (/curl|wget/.test(cmd)) {
|
|
175
|
+
allow(`network request: ${cmd}`)
|
|
176
|
+
}
|
|
177
|
+
if (/git\s+commit\s+--amend/.test(cmd)) {
|
|
178
|
+
allow(`git commit --amend`)
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// ── WRITE / EDIT tool checks ────────────────────────────────────────────────
|
|
183
|
+
if (tool_name === 'Write' || tool_name === 'Edit') {
|
|
184
|
+
const filePath = tool_input.file_path || tool_input.path || ''
|
|
185
|
+
|
|
186
|
+
// Credential files
|
|
187
|
+
if (/\.(env|pem|key|cert)$/.test(filePath)) {
|
|
188
|
+
block(`write to credential file blocked: ${filePath}`)
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Path escape -- must be in allowed paths
|
|
192
|
+
if (filePath && isOutsideAllowedPaths(filePath)) {
|
|
193
|
+
block(`path escape: ${filePath} is outside allowed paths`)
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Writes to handoff/inbox dirs -- allowed but logged
|
|
197
|
+
if (filePath.includes('_vibe-chain-output')) {
|
|
198
|
+
allow(`pipeline output write: ${filePath}`)
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// All other tools -- allow by default
|
|
203
|
+
allow(null)
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
## 2. Context file schema
|
|
207
|
+
|
|
208
|
+
Written by the forge daemon to `_vibe-chain-output/worker-inbox/<agent>/{story-id}.context.json`
|
|
209
|
+
alongside each inbox task file. Heimdall reads this on every invocation.
|
|
210
|
+
|
|
211
|
+
```json
|
|
212
|
+
{
|
|
213
|
+
"story_id": "FORGE-3",
|
|
214
|
+
"agent": "furnace",
|
|
215
|
+
"worktree_path": "/abs/path/to/worktree",
|
|
216
|
+
"assigned_branch": "feature/forge-3-events-api",
|
|
217
|
+
"handoff_dir": "/abs/path/to/_vibe-chain-output/handoffs",
|
|
218
|
+
"has_db_migration": false,
|
|
219
|
+
"has_api_changes": true,
|
|
220
|
+
"allowed_paths": [
|
|
221
|
+
"/abs/path/to/worktree",
|
|
222
|
+
"/abs/path/to/_vibe-chain-output/handoffs",
|
|
223
|
+
"/abs/path/to/_vibe-chain-output/worker-inbox"
|
|
224
|
+
]
|
|
225
|
+
}
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
All paths must be absolute. The forge daemon resolves them from `project_root` at inbox-write time.
|
|
229
|
+
|
|
230
|
+
## 3. `settings.local.json` written by daemon at worker startup
|
|
231
|
+
|
|
232
|
+
The forge daemon must write (or merge) this into `.claude/settings.local.json` in the
|
|
233
|
+
worker's working directory when starting a worker for a lab task. This installs Heimdall
|
|
234
|
+
for that worker session.
|
|
235
|
+
|
|
236
|
+
```json
|
|
237
|
+
{
|
|
238
|
+
"hooks": {
|
|
239
|
+
"PreToolUse": [
|
|
240
|
+
{
|
|
241
|
+
"matcher": "Bash",
|
|
242
|
+
"hooks": [{ "type": "command", "command": "node bin/lib/heimdall.js" }]
|
|
243
|
+
},
|
|
244
|
+
{
|
|
245
|
+
"matcher": "Write",
|
|
246
|
+
"hooks": [{ "type": "command", "command": "node bin/lib/heimdall.js" }]
|
|
247
|
+
},
|
|
248
|
+
{
|
|
249
|
+
"matcher": "Edit",
|
|
250
|
+
"hooks": [{ "type": "command", "command": "node bin/lib/heimdall.js" }]
|
|
251
|
+
}
|
|
252
|
+
]
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
Use a merge strategy: if `.claude/settings.local.json` already exists, merge the hooks
|
|
258
|
+
array rather than overwrite. Preserve any existing hooks the worker session already has.
|
|
259
|
+
|
|
260
|
+
**Implementation:** `bin/lib/daemon/heimdall-setup.js` -- called by the daemon at inbox-write time.
|
|
261
|
+
|
|
262
|
+
## 4. Escalation signal handling in forge daemon
|
|
263
|
+
|
|
264
|
+
The daemon's task monitoring loop (in `forge-daemon.sh` or the modularized equivalent)
|
|
265
|
+
must check for `.escalation` files:
|
|
266
|
+
|
|
267
|
+
```bash
|
|
268
|
+
check_heimdall_escalations() {
|
|
269
|
+
local inbox_dir="$1"
|
|
270
|
+
for escalation_file in "$inbox_dir"/**/*.escalation; do
|
|
271
|
+
[[ -f "$escalation_file" ]] || continue
|
|
272
|
+
local story_id
|
|
273
|
+
story_id=$(basename "$escalation_file" .escalation)
|
|
274
|
+
local agent_dir
|
|
275
|
+
agent_dir=$(dirname "$escalation_file")
|
|
276
|
+
local agent
|
|
277
|
+
agent=$(basename "$agent_dir")
|
|
278
|
+
|
|
279
|
+
log "HEIMDALL SOUNDED: $agent/$story_id -- writing escalation handoff"
|
|
280
|
+
|
|
281
|
+
# Write escalation handoff to lab's handoff directory
|
|
282
|
+
# Lab sentinel will route to human review on next cycle
|
|
283
|
+
write_escalation_handoff "$story_id" "$agent" "$escalation_file"
|
|
284
|
+
|
|
285
|
+
# Move escalation file to processed
|
|
286
|
+
mv "$escalation_file" "${escalation_file}.processed"
|
|
287
|
+
done
|
|
288
|
+
}
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
The escalation handoff format:
|
|
292
|
+
```yaml
|
|
293
|
+
---
|
|
294
|
+
type: worker-escalation
|
|
295
|
+
story: "{story_id}"
|
|
296
|
+
from: forge-daemon
|
|
297
|
+
to: human
|
|
298
|
+
created: {ISO8601}
|
|
299
|
+
status: pending
|
|
300
|
+
agent: "{agent}"
|
|
301
|
+
violations: {N}
|
|
302
|
+
---
|
|
303
|
+
|
|
304
|
+
## Heimdall Escalation
|
|
305
|
+
|
|
306
|
+
Worker `{agent}` accumulated {N} policy violations on story `{story_id}`.
|
|
307
|
+
The worker has been stopped for this story. Human review required.
|
|
308
|
+
|
|
309
|
+
## Last Violation
|
|
310
|
+
{reason from .escalation JSON}
|
|
311
|
+
|
|
312
|
+
## Action Required
|
|
313
|
+
Review the audit log at `heimdall-audit.log` and decide:
|
|
314
|
+
- Fix the story spec and resubmit
|
|
315
|
+
- Manually complete the work
|
|
316
|
+
- Cancel the story
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
## 5. Configuration
|
|
320
|
+
|
|
321
|
+
Add to `_vibe-chain/config.yaml` under `agent_personalities`:
|
|
322
|
+
|
|
323
|
+
```yaml
|
|
324
|
+
agent_personalities:
|
|
325
|
+
heimdall:
|
|
326
|
+
enabled: true # always on when agent_personalities is configured
|
|
327
|
+
max_violations: 3 # violations before Gjallarhorn
|
|
328
|
+
audit_log: _vibe-chain-output/heimdall-audit.log
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
## 6. Tests (`tests/heimdall.test.js`)
|
|
332
|
+
|
|
333
|
+
```javascript
|
|
334
|
+
import { describe, test, expect } from '@jest/globals'
|
|
335
|
+
// Test by invoking heimdall.js as a subprocess with mocked stdin and context file
|
|
336
|
+
|
|
337
|
+
describe('Heimdall -- hard blocks', () => {
|
|
338
|
+
test('blocks rm -rf outside worktree', async () => {
|
|
339
|
+
const result = await runHeimdall(
|
|
340
|
+
{ tool_name: 'Bash', tool_input: { command: 'rm -rf /tmp/other' } },
|
|
341
|
+
{ worktree_path: '/tmp/worktree', allowed_paths: ['/tmp/worktree'] }
|
|
342
|
+
)
|
|
343
|
+
expect(result.exitCode).toBe(2)
|
|
344
|
+
expect(result.stdout).toContain('destructive rm')
|
|
345
|
+
})
|
|
346
|
+
|
|
347
|
+
test('blocks force push', async () => {
|
|
348
|
+
const result = await runHeimdall(
|
|
349
|
+
{ tool_name: 'Bash', tool_input: { command: 'git push origin --force' } },
|
|
350
|
+
baseContext()
|
|
351
|
+
)
|
|
352
|
+
expect(result.exitCode).toBe(2)
|
|
353
|
+
})
|
|
354
|
+
|
|
355
|
+
test('blocks path escape on Write', async () => {
|
|
356
|
+
const result = await runHeimdall(
|
|
357
|
+
{ tool_name: 'Write', tool_input: { file_path: '/etc/passwd' } },
|
|
358
|
+
baseContext()
|
|
359
|
+
)
|
|
360
|
+
expect(result.exitCode).toBe(2)
|
|
361
|
+
expect(result.stdout).toContain('path escape')
|
|
362
|
+
})
|
|
363
|
+
|
|
364
|
+
test('blocks DROP TABLE without has_db_migration', async () => {
|
|
365
|
+
const result = await runHeimdall(
|
|
366
|
+
{ tool_name: 'Bash', tool_input: { command: 'sqlite3 app.db "DROP TABLE users"' } },
|
|
367
|
+
{ ...baseContext(), has_db_migration: false }
|
|
368
|
+
)
|
|
369
|
+
expect(result.exitCode).toBe(2)
|
|
370
|
+
})
|
|
371
|
+
|
|
372
|
+
test('allows DROP TABLE with has_db_migration', async () => {
|
|
373
|
+
const result = await runHeimdall(
|
|
374
|
+
{ tool_name: 'Bash', tool_input: { command: 'sqlite3 app.db "DROP TABLE old_schema"' } },
|
|
375
|
+
{ ...baseContext(), has_db_migration: true }
|
|
376
|
+
)
|
|
377
|
+
expect(result.exitCode).toBe(0)
|
|
378
|
+
})
|
|
379
|
+
})
|
|
380
|
+
|
|
381
|
+
describe('Heimdall -- audit log allows', () => {
|
|
382
|
+
test('allows npm install with audit entry', async () => {
|
|
383
|
+
const result = await runHeimdall(
|
|
384
|
+
{ tool_name: 'Bash', tool_input: { command: 'npm install lodash' } },
|
|
385
|
+
baseContext()
|
|
386
|
+
)
|
|
387
|
+
expect(result.exitCode).toBe(0)
|
|
388
|
+
// Audit log should contain the entry -- checked via log file
|
|
389
|
+
})
|
|
390
|
+
})
|
|
391
|
+
|
|
392
|
+
describe('Heimdall -- forge-native passthrough', () => {
|
|
393
|
+
test('exits 0 immediately when no context file present', async () => {
|
|
394
|
+
const result = await runHeimdall(
|
|
395
|
+
{ tool_name: 'Bash', tool_input: { command: 'rm -rf /' } },
|
|
396
|
+
null // no context file
|
|
397
|
+
)
|
|
398
|
+
expect(result.exitCode).toBe(0)
|
|
399
|
+
})
|
|
400
|
+
})
|
|
401
|
+
|
|
402
|
+
describe('Heimdall -- escalation', () => {
|
|
403
|
+
test('writes .escalation file after N violations', async () => {
|
|
404
|
+
// Run N blocking actions, check .escalation exists on Nth
|
|
405
|
+
})
|
|
406
|
+
})
|
|
407
|
+
```
|
|
408
|
+
|
|
409
|
+
---
|
|
410
|
+
|
|
411
|
+
# Notes
|
|
412
|
+
|
|
413
|
+
- Heimdall must be fast. It runs on every tool call. Target < 50ms per invocation.
|
|
414
|
+
Read context file once per process start (it is passed as a singleton call).
|
|
415
|
+
Do not make network calls.
|
|
416
|
+
- The `bin/lib/heimdall.js` path in the hook command must be absolute or resolvable
|
|
417
|
+
from the worker's working directory. Use `process.env.FORGE_BIN_DIR` if needed.
|
|
418
|
+
- PLAT-1 is a prerequisite for IPC-7 (background worker daemon mode).
|
|
419
|
+
It also applies to LAB-FORGE-3 (degraded spawn) -- LAB-FORGE-3 must write
|
|
420
|
+
`.claude/settings.local.json` at spawn time, same as IPC workers.
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: SEC-001
|
|
3
|
+
title: Fix SQL Injection vulnerabilities in database.sh
|
|
4
|
+
type: security
|
|
5
|
+
priority: critical
|
|
6
|
+
status: completed
|
|
7
|
+
assigned_to: aegis
|
|
8
|
+
created_at: 2026-01-15T16:30:00Z
|
|
9
|
+
created_by: security-review
|
|
10
|
+
completed_at: 2026-01-15T17:30:00Z
|
|
11
|
+
completed_by: aegis
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## Summary
|
|
15
|
+
|
|
16
|
+
Multiple SQLite queries in `bin/lib/database.sh` interpolate shell variables directly into SQL strings without proper escaping or parameterization.
|
|
17
|
+
|
|
18
|
+
## Affected Lines
|
|
19
|
+
|
|
20
|
+
- Line 93: `db_set_daemon_state()` - `$new_state` interpolated
|
|
21
|
+
- Lines 136-145: `db_upsert_agent_status()` - `$agent`, `$status`, `$task`, `$message`, `$updated_at` interpolated
|
|
22
|
+
- Line 151: `db_get_agent_mtime()` - `$agent` interpolated
|
|
23
|
+
- Line 166: `db_get_agents_by_status()` - `$status` interpolated
|
|
24
|
+
|
|
25
|
+
## Attack Scenario
|
|
26
|
+
|
|
27
|
+
A malicious agent status JSON file could contain:
|
|
28
|
+
```json
|
|
29
|
+
{"agent": "'; DROP TABLE agent_status; --", "status": "working"}
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Remediation Applied
|
|
33
|
+
|
|
34
|
+
1. Created `db_escape()` function that:
|
|
35
|
+
- Removes null bytes
|
|
36
|
+
- Escapes single quotes by doubling them (`'` -> `''`)
|
|
37
|
+
- Removes backslashes that could escape quotes
|
|
38
|
+
|
|
39
|
+
2. Created `db_validate_identifier()` for validating identifiers
|
|
40
|
+
|
|
41
|
+
3. Applied `db_escape()` to all user-controlled variables in:
|
|
42
|
+
- `db_upsert_agent_status()` - all string parameters
|
|
43
|
+
- `db_get_agent_mtime()` - agent parameter
|
|
44
|
+
- `db_get_agents_by_status()` - status parameter
|
|
45
|
+
- `db_record_status_history()` - all parameters
|
|
46
|
+
|
|
47
|
+
4. Added numeric validation for:
|
|
48
|
+
- `file_mtime` in `db_upsert_agent_status()`
|
|
49
|
+
- `minutes` in `db_cleanup_stale_agents()`
|
|
50
|
+
- `days` in `db_prune_history()`
|
|
51
|
+
|
|
52
|
+
5. Added whitelist validation for `db_set_daemon_state()` - only allows `active|idle|stopped`
|
|
53
|
+
|
|
54
|
+
## Acceptance Criteria
|
|
55
|
+
|
|
56
|
+
- [x] All SQL queries use escaped or parameterized inputs
|
|
57
|
+
- [x] Numeric parameters validated before use
|
|
58
|
+
- [x] State values whitelist-validated
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: SEC-002
|
|
3
|
+
title: Fix Command Injection in system notifications
|
|
4
|
+
type: security
|
|
5
|
+
priority: critical
|
|
6
|
+
status: completed
|
|
7
|
+
assigned_to: aegis
|
|
8
|
+
created_at: 2026-01-15T16:30:00Z
|
|
9
|
+
created_by: security-review
|
|
10
|
+
completed_at: 2026-01-15T17:30:00Z
|
|
11
|
+
completed_by: aegis
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## Summary
|
|
15
|
+
|
|
16
|
+
The `send_system_notification()` function in `bin/forge-daemon.sh` directly interpolates `$message` into PowerShell and osascript commands without sanitization.
|
|
17
|
+
|
|
18
|
+
## Affected Lines
|
|
19
|
+
|
|
20
|
+
- Lines 146-154: Windows PowerShell - `$message` in CreateTextNode()
|
|
21
|
+
- Line 158: macOS osascript - `$message` in display notification
|
|
22
|
+
|
|
23
|
+
## Attack Scenario
|
|
24
|
+
|
|
25
|
+
Malicious task file:
|
|
26
|
+
```yaml
|
|
27
|
+
title: "'); Start-Process calc; #"
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
When extracted and passed to notification, executes arbitrary commands.
|
|
31
|
+
|
|
32
|
+
## Remediation Applied
|
|
33
|
+
|
|
34
|
+
1. Created `sanitize_notification_message()` function that:
|
|
35
|
+
- Removes null bytes
|
|
36
|
+
- Strips all characters except: alphanumeric, spaces, periods, commas, colons, semicolons, exclamation/question marks, hyphens, underscores
|
|
37
|
+
- Limits message length to 200 characters
|
|
38
|
+
|
|
39
|
+
2. Applied sanitization in `send_system_notification()` before any shell interpolation
|
|
40
|
+
|
|
41
|
+
## Acceptance Criteria
|
|
42
|
+
|
|
43
|
+
- [x] All notification messages sanitized before shell interpolation
|
|
44
|
+
- [x] Special characters stripped
|
|
45
|
+
- [x] Notifications still display readable messages (safe chars preserved)
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: SEC-003
|
|
3
|
+
title: Fix eval injection in agent config loading
|
|
4
|
+
type: security
|
|
5
|
+
priority: high
|
|
6
|
+
status: completed
|
|
7
|
+
assigned_to: aegis
|
|
8
|
+
created_at: 2026-01-15T16:30:00Z
|
|
9
|
+
created_by: security-review
|
|
10
|
+
completed_at: 2026-01-15T17:30:00Z
|
|
11
|
+
completed_by: aegis
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## Summary
|
|
15
|
+
|
|
16
|
+
The `load_agents_from_json()` function in `bin/lib/config.sh` uses `eval` to execute shell variable assignments generated from JSON file contents.
|
|
17
|
+
|
|
18
|
+
## Affected Lines
|
|
19
|
+
|
|
20
|
+
- Line 100: `eval "$agent_data"`
|
|
21
|
+
|
|
22
|
+
## Attack Scenario
|
|
23
|
+
|
|
24
|
+
Malicious `config/agents.json`:
|
|
25
|
+
```json
|
|
26
|
+
{
|
|
27
|
+
"agents": {
|
|
28
|
+
"anvil$(whoami>/tmp/pwned)": {
|
|
29
|
+
"name": "Anvil"
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Command substitution executes when `eval` runs.
|
|
36
|
+
|
|
37
|
+
## Remediation Applied
|
|
38
|
+
|
|
39
|
+
1. Added `isValidIdentifier()` function in Node.js that validates names match `^[a-z0-9_-]+$`
|
|
40
|
+
|
|
41
|
+
2. Added `escapeForShell()` function that escapes dangerous characters in string values:
|
|
42
|
+
- Backslashes, double quotes, dollar signs, backticks, newlines
|
|
43
|
+
|
|
44
|
+
3. All agent names and aliases are validated before output:
|
|
45
|
+
- Invalid names cause immediate exit with clear error message
|
|
46
|
+
|
|
47
|
+
4. All string values (display names, roles, icons, etc.) are escaped before output
|
|
48
|
+
|
|
49
|
+
## Acceptance Criteria
|
|
50
|
+
|
|
51
|
+
- [x] Agent names validated to alphanumeric + underscore/hyphen only
|
|
52
|
+
- [x] Special characters in agents.json cause clear error, not execution
|
|
53
|
+
- [x] Existing valid configurations continue to work
|
|
54
|
+
- [x] String values properly escaped for shell safety
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: SEC-004
|
|
3
|
+
title: Fix TOCTOU race condition in PID file handling
|
|
4
|
+
type: security
|
|
5
|
+
priority: high
|
|
6
|
+
status: completed
|
|
7
|
+
assigned_to: aegis
|
|
8
|
+
created_at: 2026-01-15T16:30:00Z
|
|
9
|
+
created_by: security-review
|
|
10
|
+
completed_at: 2026-01-15T17:30:00Z
|
|
11
|
+
completed_by: aegis
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## Summary
|
|
15
|
+
|
|
16
|
+
The daemon start logic in `bin/forge-daemon.sh` has a Time-of-Check-Time-of-Use (TOCTOU) race condition between checking if the daemon is running and writing the new PID file.
|
|
17
|
+
|
|
18
|
+
## Affected Lines
|
|
19
|
+
|
|
20
|
+
- Lines 541-565: `cmd_start()` - race window between check and PID write
|
|
21
|
+
|
|
22
|
+
## Attack Scenario
|
|
23
|
+
|
|
24
|
+
Two simultaneous `forge daemon start` commands could both pass the initial check, leading to:
|
|
25
|
+
- Multiple daemon instances
|
|
26
|
+
- PID file corruption
|
|
27
|
+
- Unpredictable behavior
|
|
28
|
+
|
|
29
|
+
## Remediation Applied
|
|
30
|
+
|
|
31
|
+
1. Added flock-based exclusive locking at the start of `cmd_start()`:
|
|
32
|
+
- Creates `startup.lock` file in `.forge/` directory
|
|
33
|
+
- Uses file descriptor 200 for the lock
|
|
34
|
+
- Non-blocking lock attempt (`flock -n`)
|
|
35
|
+
- Lock automatically released when script exits
|
|
36
|
+
|
|
37
|
+
2. Graceful fallback when flock is not available:
|
|
38
|
+
- Falls back to existing PID-based check (less secure but functional)
|
|
39
|
+
- Maintains cross-platform compatibility
|
|
40
|
+
|
|
41
|
+
3. Defense in depth:
|
|
42
|
+
- Existing LOCK_FILE check remains as secondary protection
|
|
43
|
+
|
|
44
|
+
## Acceptance Criteria
|
|
45
|
+
|
|
46
|
+
- [x] Use flock or equivalent for atomic lock acquisition
|
|
47
|
+
- [x] Only one daemon instance can start at a time
|
|
48
|
+
- [x] Lock released properly on daemon exit (automatic via fd close)
|
|
49
|
+
- [x] Works on Windows (Git Bash), macOS, Linux (with fallback)
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: SEC-005
|
|
3
|
+
title: Fix path traversal in worker-loop hook
|
|
4
|
+
type: security
|
|
5
|
+
priority: high
|
|
6
|
+
status: completed
|
|
7
|
+
assigned_to: aegis
|
|
8
|
+
created_at: 2026-01-15T16:30:00Z
|
|
9
|
+
created_by: security-review
|
|
10
|
+
completed_at: 2026-01-15T17:30:00Z
|
|
11
|
+
completed_by: aegis
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## Summary
|
|
15
|
+
|
|
16
|
+
The worker-loop hook in `.claude/hooks/worker-loop.sh` defaults `FORGE_ROOT` to the current working directory without validation.
|
|
17
|
+
|
|
18
|
+
## Affected Lines
|
|
19
|
+
|
|
20
|
+
- Line 20: `FORGE_ROOT="${FORGE_ROOT:-$(pwd)}"`
|
|
21
|
+
|
|
22
|
+
## Attack Scenario
|
|
23
|
+
|
|
24
|
+
If the hook executes in an attacker-controlled directory (e.g., `/tmp/malicious`), task file checks could be redirected:
|
|
25
|
+
```
|
|
26
|
+
FORGE_ROOT=/tmp/malicious
|
|
27
|
+
$TASKS_DIR="/tmp/malicious/tasks"
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Attacker could place malicious task files that get processed.
|
|
31
|
+
|
|
32
|
+
## Remediation Applied
|
|
33
|
+
|
|
34
|
+
1. If FORGE_ROOT is set via environment:
|
|
35
|
+
- Validate it contains `.forge/` or `tasks/` directory
|
|
36
|
+
- Reject with safe JSON response if validation fails
|
|
37
|
+
|
|
38
|
+
2. If FORGE_ROOT is not set:
|
|
39
|
+
- Derive from script location (`.claude/hooks/` -> `../..`)
|
|
40
|
+
- Validate derived path contains expected directories
|
|
41
|
+
- Fall back to pwd only if it passes validation
|
|
42
|
+
|
|
43
|
+
3. Safe failure mode:
|
|
44
|
+
- Returns `{"decision": "allow"}` with error message on validation failure
|
|
45
|
+
- Does not process potentially malicious task directories
|
|
46
|
+
|
|
47
|
+
## Acceptance Criteria
|
|
48
|
+
|
|
49
|
+
- [x] FORGE_ROOT validated before use
|
|
50
|
+
- [x] Hook fails safely if run from invalid directory
|
|
51
|
+
- [x] Normal operation unaffected when run correctly
|