create-openthrottle 1.0.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/index.mjs +335 -0
- package/package.json +33 -0
- package/templates/docker/Dockerfile +25 -0
- package/templates/docker/agent-lib.sh +223 -0
- package/templates/docker/entrypoint.sh +285 -0
- package/templates/docker/git-hooks/pre-push +15 -0
- package/templates/docker/hooks/auto-format.sh +44 -0
- package/templates/docker/hooks/block-push-to-main.sh +116 -0
- package/templates/docker/hooks/log-commands.sh +48 -0
- package/templates/docker/run-builder.sh +351 -0
- package/templates/docker/run-reviewer.sh +251 -0
- package/templates/docker/task-adapter.sh +123 -0
- package/templates/wake-sandbox.yml +146 -0
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# =============================================================================
|
|
3
|
+
# entrypoint.sh — Daytona sandbox entrypoint for openthrottle
|
|
4
|
+
#
|
|
5
|
+
# Replaces the 400-line bootstrap.sh with a simple config-driven flow.
|
|
6
|
+
# Runs as root, configures the environment, then drops to the daytona user.
|
|
7
|
+
# =============================================================================
|
|
8
|
+
|
|
9
|
+
set -euo pipefail
|
|
10
|
+
|
|
11
|
+
: "${GITHUB_REPO:?GITHUB_REPO is required}"
|
|
12
|
+
: "${GITHUB_TOKEN:?GITHUB_TOKEN is required}"
|
|
13
|
+
: "${TASK_TYPE:?TASK_TYPE is required}"
|
|
14
|
+
: "${WORK_ITEM:?WORK_ITEM is required}"
|
|
15
|
+
|
|
16
|
+
SANDBOX_HOME="/home/daytona"
|
|
17
|
+
REPO="${SANDBOX_HOME}/repo"
|
|
18
|
+
|
|
19
|
+
log() { echo "[entrypoint $(date +%H:%M:%S)] $1"; }
|
|
20
|
+
|
|
21
|
+
seal_file() {
|
|
22
|
+
local FILE="$1"
|
|
23
|
+
if chattr +i "$FILE" 2>/dev/null; then
|
|
24
|
+
log "Sealed: $FILE (immutable)"
|
|
25
|
+
else
|
|
26
|
+
chown root:root "$FILE"
|
|
27
|
+
chmod 444 "$FILE"
|
|
28
|
+
log "WARNING: chattr +i not supported — sealed $FILE with permissions (weaker)"
|
|
29
|
+
fi
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
# ---------------------------------------------------------------------------
|
|
33
|
+
# 1. Clone repo
|
|
34
|
+
# ---------------------------------------------------------------------------
|
|
35
|
+
log "Cloning ${GITHUB_REPO}"
|
|
36
|
+
gh repo clone "$GITHUB_REPO" "$REPO" -- --depth=50
|
|
37
|
+
cd "$REPO"
|
|
38
|
+
|
|
39
|
+
# ---------------------------------------------------------------------------
|
|
40
|
+
# 2. Read .openthrottle.yml
|
|
41
|
+
# ---------------------------------------------------------------------------
|
|
42
|
+
CONFIG="${REPO}/.openthrottle.yml"
|
|
43
|
+
if [[ ! -f "$CONFIG" ]]; then
|
|
44
|
+
log "FATAL: .openthrottle.yml not found in repo"
|
|
45
|
+
exit 1
|
|
46
|
+
fi
|
|
47
|
+
|
|
48
|
+
read_config() {
|
|
49
|
+
local result
|
|
50
|
+
result=$(yq -r "$1 // \"$2\"" "$CONFIG") || {
|
|
51
|
+
log "FATAL: Failed to read config key $1 from $CONFIG"
|
|
52
|
+
exit 1
|
|
53
|
+
}
|
|
54
|
+
echo "$result"
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
BASE_BRANCH=$(read_config '.base_branch' 'main')
|
|
58
|
+
TEST_CMD=$(read_config '.test' '')
|
|
59
|
+
LINT_CMD=$(read_config '.lint' '')
|
|
60
|
+
AGENT=$(read_config '.agent' 'claude')
|
|
61
|
+
|
|
62
|
+
# ---------------------------------------------------------------------------
|
|
63
|
+
# 3. Run post_bootstrap commands (as daytona user, not root)
|
|
64
|
+
# ---------------------------------------------------------------------------
|
|
65
|
+
POST_BOOTSTRAP=$(yq -r '.post_bootstrap // [] | .[]' "$CONFIG") || {
|
|
66
|
+
log "FATAL: Failed to parse post_bootstrap from .openthrottle.yml — check YAML syntax"
|
|
67
|
+
exit 1
|
|
68
|
+
}
|
|
69
|
+
if [[ -n "$POST_BOOTSTRAP" ]]; then
|
|
70
|
+
log "Running post_bootstrap"
|
|
71
|
+
while IFS= read -r cmd; do
|
|
72
|
+
log " > $cmd"
|
|
73
|
+
gosu daytona bash -c "$cmd" || {
|
|
74
|
+
log "FATAL: post_bootstrap command failed (exit $?): $cmd"
|
|
75
|
+
exit 1
|
|
76
|
+
}
|
|
77
|
+
done <<< "$POST_BOOTSTRAP"
|
|
78
|
+
fi
|
|
79
|
+
|
|
80
|
+
# ---------------------------------------------------------------------------
|
|
81
|
+
# 4. Configure agent settings (per-agent)
|
|
82
|
+
# ---------------------------------------------------------------------------
|
|
83
|
+
log "Configuring agent settings (${AGENT})"
|
|
84
|
+
|
|
85
|
+
# 4a. Install universal git hooks and seal git config (works for ALL agent runtimes)
|
|
86
|
+
git -C "$REPO" config core.hooksPath /opt/openthrottle/git-hooks
|
|
87
|
+
log "Installed git hooks (pre-push)"
|
|
88
|
+
# Seal .git/config to prevent agents from changing core.hooksPath
|
|
89
|
+
seal_file "${REPO}/.git/config" 2>/dev/null || true
|
|
90
|
+
|
|
91
|
+
# 4b. Per-agent configuration
|
|
92
|
+
case "$AGENT" in
|
|
93
|
+
claude)
|
|
94
|
+
SETTINGS_DIR="${SANDBOX_HOME}/.claude"
|
|
95
|
+
mkdir -p "$SETTINGS_DIR"
|
|
96
|
+
|
|
97
|
+
# Build stop hooks from config
|
|
98
|
+
STOP_HOOKS="[]"
|
|
99
|
+
if [[ -n "$LINT_CMD" ]] || [[ -n "$TEST_CMD" ]]; then
|
|
100
|
+
STOP_CMDS="[]"
|
|
101
|
+
[[ -n "$LINT_CMD" ]] && STOP_CMDS=$(echo "$STOP_CMDS" | jq --arg c "$LINT_CMD" '. + [$c]')
|
|
102
|
+
[[ -n "$TEST_CMD" ]] && STOP_CMDS=$(echo "$STOP_CMDS" | jq --arg c "$TEST_CMD" '. + [$c]')
|
|
103
|
+
STOP_HOOKS=$(echo "$STOP_CMDS" | jq '[{"hooks": .}]')
|
|
104
|
+
fi
|
|
105
|
+
|
|
106
|
+
# Build base settings with hooks
|
|
107
|
+
jq -n \
|
|
108
|
+
--argjson stop_hooks "$STOP_HOOKS" \
|
|
109
|
+
'{
|
|
110
|
+
"permissions": {"allow": [], "deny": []},
|
|
111
|
+
"hooks": {
|
|
112
|
+
"PreToolUse": [
|
|
113
|
+
{"matcher": "Bash", "hooks": ["/opt/openthrottle/hooks/block-push-to-main.sh"]}
|
|
114
|
+
],
|
|
115
|
+
"PostToolUse": [
|
|
116
|
+
{"matcher": "Bash", "hooks": ["/opt/openthrottle/hooks/log-commands.sh"]},
|
|
117
|
+
{"matcher": "Write|Edit", "hooks": ["/opt/openthrottle/hooks/auto-format.sh"]}
|
|
118
|
+
],
|
|
119
|
+
"Stop": $stop_hooks
|
|
120
|
+
}
|
|
121
|
+
}' > "${SETTINGS_DIR}/settings.json"
|
|
122
|
+
|
|
123
|
+
# Merge default MCP servers (Telegram, Context7)
|
|
124
|
+
DEFAULT_MCPS=$(jq -n '{
|
|
125
|
+
"telegram": {
|
|
126
|
+
"command": "npx",
|
|
127
|
+
"args": ["-y", "@punkpeye/telegram-mcp"],
|
|
128
|
+
"env": {
|
|
129
|
+
"TELEGRAM_BOT_TOKEN": env.TELEGRAM_BOT_TOKEN,
|
|
130
|
+
"TELEGRAM_CHAT_ID": env.TELEGRAM_CHAT_ID
|
|
131
|
+
}
|
|
132
|
+
},
|
|
133
|
+
"context7": {
|
|
134
|
+
"command": "npx",
|
|
135
|
+
"args": ["-y", "@upstash/context7-mcp"]
|
|
136
|
+
}
|
|
137
|
+
}')
|
|
138
|
+
|
|
139
|
+
# Merge project-specific MCP servers, resolving "from-env" placeholders
|
|
140
|
+
PROJECT_MCPS=$(yq -o=json '.mcp_servers // {}' "$CONFIG") || {
|
|
141
|
+
log "WARNING: Failed to parse mcp_servers — project MCP servers will not be configured"
|
|
142
|
+
PROJECT_MCPS='{}'
|
|
143
|
+
}
|
|
144
|
+
if [[ "$PROJECT_MCPS" != "{}" ]]; then
|
|
145
|
+
PROJECT_MCPS=$(echo "$PROJECT_MCPS" | jq '
|
|
146
|
+
walk(if type == "object" then
|
|
147
|
+
with_entries(
|
|
148
|
+
if .value == "from-env" then
|
|
149
|
+
.value = (env[.key] // null) |
|
|
150
|
+
if .value == null then
|
|
151
|
+
error("Environment variable \(.key) is required by mcp_servers config but not set")
|
|
152
|
+
else . end
|
|
153
|
+
else . end
|
|
154
|
+
)
|
|
155
|
+
else . end)
|
|
156
|
+
') || {
|
|
157
|
+
log "FATAL: Missing required environment variable for MCP server configuration"
|
|
158
|
+
exit 1
|
|
159
|
+
}
|
|
160
|
+
fi
|
|
161
|
+
|
|
162
|
+
# Merge all MCP servers into settings
|
|
163
|
+
ALL_MCPS=$(echo "$DEFAULT_MCPS" "$PROJECT_MCPS" | jq -s '.[0] * .[1]')
|
|
164
|
+
jq --argjson mcps "$ALL_MCPS" '.mcpServers = $mcps' \
|
|
165
|
+
"${SETTINGS_DIR}/settings.json" > "${SETTINGS_DIR}/settings.json.tmp" \
|
|
166
|
+
&& mv "${SETTINGS_DIR}/settings.json.tmp" "${SETTINGS_DIR}/settings.json"
|
|
167
|
+
|
|
168
|
+
# Apply Supabase tool allowlist if supabase MCP is configured
|
|
169
|
+
if echo "$ALL_MCPS" | jq -e '.supabase' > /dev/null 2>&1; then
|
|
170
|
+
log "Supabase MCP detected — applying tool allowlist"
|
|
171
|
+
jq '.permissions.allow += [
|
|
172
|
+
"mcp__supabase__create_branch",
|
|
173
|
+
"mcp__supabase__delete_branch",
|
|
174
|
+
"mcp__supabase__list_branches",
|
|
175
|
+
"mcp__supabase__reset_branch",
|
|
176
|
+
"mcp__supabase__list_tables",
|
|
177
|
+
"mcp__supabase__get_schemas",
|
|
178
|
+
"mcp__supabase__list_migrations",
|
|
179
|
+
"mcp__supabase__get_project_url",
|
|
180
|
+
"mcp__supabase__search_docs",
|
|
181
|
+
"mcp__supabase__get_logs"
|
|
182
|
+
] | .permissions.deny += [
|
|
183
|
+
"mcp__supabase__execute_sql",
|
|
184
|
+
"mcp__supabase__apply_migration",
|
|
185
|
+
"mcp__supabase__deploy_edge_function",
|
|
186
|
+
"mcp__supabase__merge_branch"
|
|
187
|
+
]' "${SETTINGS_DIR}/settings.json" > "${SETTINGS_DIR}/settings.json.tmp" \
|
|
188
|
+
&& mv "${SETTINGS_DIR}/settings.json.tmp" "${SETTINGS_DIR}/settings.json"
|
|
189
|
+
fi
|
|
190
|
+
;;
|
|
191
|
+
|
|
192
|
+
codex)
|
|
193
|
+
SETTINGS_DIR="${SANDBOX_HOME}/.codex"
|
|
194
|
+
mkdir -p "$SETTINGS_DIR"
|
|
195
|
+
# Codex reads AGENTS.md for project instructions
|
|
196
|
+
# No equivalent to Claude's settings.json hooks — git hooks provide safety
|
|
197
|
+
log "Codex configured (safety via git hooks)"
|
|
198
|
+
;;
|
|
199
|
+
|
|
200
|
+
aider)
|
|
201
|
+
SETTINGS_DIR="${SANDBOX_HOME}"
|
|
202
|
+
# Write Aider config
|
|
203
|
+
cat > "${SANDBOX_HOME}/.aider.conf.yml" <<AIDEREOF
|
|
204
|
+
auto-commits: false
|
|
205
|
+
model: ${AGENT_MODEL:-claude-sonnet-4-20250514}
|
|
206
|
+
AIDEREOF
|
|
207
|
+
log "Aider configured (safety via git hooks)"
|
|
208
|
+
;;
|
|
209
|
+
|
|
210
|
+
*)
|
|
211
|
+
log "WARNING: Unknown agent '${AGENT}' — using git hooks only for safety"
|
|
212
|
+
SETTINGS_DIR="${SANDBOX_HOME}"
|
|
213
|
+
;;
|
|
214
|
+
esac
|
|
215
|
+
|
|
216
|
+
# ---------------------------------------------------------------------------
|
|
217
|
+
# 5. Seal settings (immutable — only root can undo)
|
|
218
|
+
# ---------------------------------------------------------------------------
|
|
219
|
+
|
|
220
|
+
# Seal agent-specific settings
|
|
221
|
+
if [[ "$AGENT" == "claude" ]]; then
|
|
222
|
+
seal_file "${SETTINGS_DIR}/settings.json"
|
|
223
|
+
touch "${SETTINGS_DIR}/settings.local.json"
|
|
224
|
+
seal_file "${SETTINGS_DIR}/settings.local.json"
|
|
225
|
+
|
|
226
|
+
# Nullify repo-level .claude/settings.json (prevent agent overrides)
|
|
227
|
+
if [[ -f "${REPO}/.claude/settings.json" ]]; then
|
|
228
|
+
: > "${REPO}/.claude/settings.json"
|
|
229
|
+
seal_file "${REPO}/.claude/settings.json"
|
|
230
|
+
fi
|
|
231
|
+
elif [[ "$AGENT" == "aider" ]] && [[ -f "${SANDBOX_HOME}/.aider.conf.yml" ]]; then
|
|
232
|
+
seal_file "${SANDBOX_HOME}/.aider.conf.yml"
|
|
233
|
+
fi
|
|
234
|
+
|
|
235
|
+
# ---------------------------------------------------------------------------
|
|
236
|
+
# 7. Fix ownership (skip sealed files — chattr prevents chown on them)
|
|
237
|
+
# ---------------------------------------------------------------------------
|
|
238
|
+
chown -R daytona:daytona "$SANDBOX_HOME" 2>/dev/null || true
|
|
239
|
+
|
|
240
|
+
# ---------------------------------------------------------------------------
|
|
241
|
+
# 8. Start heartbeat (resets autoStopInterval every 5 min)
|
|
242
|
+
#
|
|
243
|
+
# Daytona auto-stop only resets on Toolbox SDK API calls, NOT on internal
|
|
244
|
+
# process activity. The Toolbox agent runs inside the sandbox on port 63650.
|
|
245
|
+
# A lightweight filesystem list call keeps the sandbox alive.
|
|
246
|
+
# ---------------------------------------------------------------------------
|
|
247
|
+
(
|
|
248
|
+
TOOLBOX_PORT="${DAYTONA_TOOLBOX_PORT:-63650}"
|
|
249
|
+
FAIL_COUNT=0
|
|
250
|
+
MAX_CONSECUTIVE_FAILURES=3
|
|
251
|
+
while true; do
|
|
252
|
+
sleep 300
|
|
253
|
+
if curl -sf "http://localhost:${TOOLBOX_PORT}/filesystem/list?path=/" > /dev/null 2>&1; then
|
|
254
|
+
FAIL_COUNT=0
|
|
255
|
+
else
|
|
256
|
+
FAIL_COUNT=$((FAIL_COUNT + 1))
|
|
257
|
+
echo "[heartbeat $(date +%H:%M:%S)] WARNING: Toolbox heartbeat failed (attempt ${FAIL_COUNT}/${MAX_CONSECUTIVE_FAILURES})" >&2
|
|
258
|
+
if [[ $FAIL_COUNT -ge $MAX_CONSECUTIVE_FAILURES ]]; then
|
|
259
|
+
echo "[heartbeat $(date +%H:%M:%S)] CRITICAL: Toolbox heartbeat failed ${MAX_CONSECUTIVE_FAILURES} times. Sandbox may auto-stop." >&2
|
|
260
|
+
fi
|
|
261
|
+
fi
|
|
262
|
+
done
|
|
263
|
+
) &
|
|
264
|
+
HEARTBEAT_PID=$!
|
|
265
|
+
|
|
266
|
+
# ---------------------------------------------------------------------------
|
|
267
|
+
# 9. Drop to daytona user and run the appropriate runner
|
|
268
|
+
# ---------------------------------------------------------------------------
|
|
269
|
+
log "Task: ${TASK_TYPE} #${WORK_ITEM} (agent: ${AGENT})"
|
|
270
|
+
|
|
271
|
+
export SANDBOX_HOME REPO BASE_BRANCH AGENT
|
|
272
|
+
export AGENT_RUNTIME="$AGENT"
|
|
273
|
+
|
|
274
|
+
case "$TASK_TYPE" in
|
|
275
|
+
prd|bug|review-fix)
|
|
276
|
+
exec gosu daytona /opt/openthrottle/run-builder.sh
|
|
277
|
+
;;
|
|
278
|
+
review|investigation)
|
|
279
|
+
exec gosu daytona /opt/openthrottle/run-reviewer.sh
|
|
280
|
+
;;
|
|
281
|
+
*)
|
|
282
|
+
log "Unknown TASK_TYPE: ${TASK_TYPE}"
|
|
283
|
+
exit 1
|
|
284
|
+
;;
|
|
285
|
+
esac
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# pre-push — Universal git hook that works for ANY agent runtime.
|
|
3
|
+
# Blocks push to main/master and force push.
|
|
4
|
+
# Installed by entrypoint.sh via git config core.hooksPath.
|
|
5
|
+
|
|
6
|
+
while read -r local_ref local_sha remote_ref remote_sha; do
|
|
7
|
+
# Block push to main/master
|
|
8
|
+
if [[ "$remote_ref" =~ refs/heads/(main|master)$ ]]; then
|
|
9
|
+
echo "BLOCKED: Direct push to main/master is not allowed." >&2
|
|
10
|
+
echo "Use: git push origin HEAD (pushes the current feature branch)" >&2
|
|
11
|
+
exit 1
|
|
12
|
+
fi
|
|
13
|
+
done
|
|
14
|
+
|
|
15
|
+
exit 0
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# auto-format.sh
|
|
3
|
+
# PostToolUse hook — fires after Write or Edit tool calls.
|
|
4
|
+
# Runs the project's formatter on any changed file if one is configured.
|
|
5
|
+
|
|
6
|
+
INPUT=$(cat)
|
|
7
|
+
FILE=$(echo "$INPUT" | jq -r '.tool_input.file_path // ""')
|
|
8
|
+
|
|
9
|
+
[[ -z "$FILE" ]] && exit 0
|
|
10
|
+
[[ ! -f "$FILE" ]] && exit 0
|
|
11
|
+
|
|
12
|
+
SANDBOX_HOME="${SANDBOX_HOME:-/home/daytona}"
|
|
13
|
+
cd "${SANDBOX_HOME}/repo" 2>/dev/null || exit 0
|
|
14
|
+
|
|
15
|
+
EXT="${FILE##*.}"
|
|
16
|
+
|
|
17
|
+
case "$EXT" in
|
|
18
|
+
ts|tsx|js|jsx|mjs|cjs|json|css|scss|html|md|yaml|yml)
|
|
19
|
+
if [[ -f "node_modules/.bin/prettier" ]]; then
|
|
20
|
+
node_modules/.bin/prettier --write "$FILE" --log-level silent 2>/dev/null
|
|
21
|
+
elif command -v prettier &>/dev/null; then
|
|
22
|
+
prettier --write "$FILE" --log-level silent 2>/dev/null
|
|
23
|
+
fi
|
|
24
|
+
;;
|
|
25
|
+
py)
|
|
26
|
+
if command -v black &>/dev/null; then
|
|
27
|
+
black "$FILE" --quiet 2>/dev/null
|
|
28
|
+
elif command -v ruff &>/dev/null; then
|
|
29
|
+
ruff format "$FILE" --quiet 2>/dev/null
|
|
30
|
+
fi
|
|
31
|
+
;;
|
|
32
|
+
go)
|
|
33
|
+
if command -v gofmt &>/dev/null; then
|
|
34
|
+
gofmt -w "$FILE" 2>/dev/null
|
|
35
|
+
fi
|
|
36
|
+
;;
|
|
37
|
+
rb)
|
|
38
|
+
if command -v rubocop &>/dev/null; then
|
|
39
|
+
rubocop -a "$FILE" --no-color 2>/dev/null
|
|
40
|
+
fi
|
|
41
|
+
;;
|
|
42
|
+
esac
|
|
43
|
+
|
|
44
|
+
exit 0
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# sandbox-guard.sh
|
|
3
|
+
# PreToolUse hook — fires before every Bash command.
|
|
4
|
+
# Enforces safety guardrails for autonomous agent execution.
|
|
5
|
+
#
|
|
6
|
+
# IMPORTANT: This guard fails CLOSED — if input cannot be parsed,
|
|
7
|
+
# the command is DENIED. A security guard must never fail open.
|
|
8
|
+
#
|
|
9
|
+
# Blocks:
|
|
10
|
+
# 1. Pushes to main/master
|
|
11
|
+
# 2. Force pushes to any branch
|
|
12
|
+
# 3. Settings.json / git config tampering
|
|
13
|
+
# 4. Git remote manipulation (prevents push to attacker-controlled remotes)
|
|
14
|
+
# 5. Secret exfiltration via curl/wget/nc/gh api (env var references in outbound calls)
|
|
15
|
+
# 6. /proc/self/environ reads in outbound contexts
|
|
16
|
+
# 7. Direct reads of .env files in outbound contexts
|
|
17
|
+
|
|
18
|
+
INPUT=$(cat)
|
|
19
|
+
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // ""') || {
|
|
20
|
+
echo "BLOCKED: Could not parse hook input — denying command for safety." >&2
|
|
21
|
+
exit 2
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if [[ -z "$COMMAND" ]]; then
|
|
25
|
+
echo "BLOCKED: Empty command in hook input — denying for safety." >&2
|
|
26
|
+
exit 2
|
|
27
|
+
fi
|
|
28
|
+
|
|
29
|
+
# ---------------------------------------------------------------------------
|
|
30
|
+
# Git safety
|
|
31
|
+
# ---------------------------------------------------------------------------
|
|
32
|
+
|
|
33
|
+
# Block pushes to main/master via any syntax (including refspec HEAD:main)
|
|
34
|
+
if echo "$COMMAND" | grep -qE 'git\s+push\b.*\b(main|master)\b'; then
|
|
35
|
+
echo "BLOCKED: Direct push to main/master is not allowed in the pipeline." >&2
|
|
36
|
+
echo "Use: git push origin HEAD (pushes the current feature branch)" >&2
|
|
37
|
+
exit 2
|
|
38
|
+
fi
|
|
39
|
+
|
|
40
|
+
# Block force push to any branch — match flags anywhere in command
|
|
41
|
+
if echo "$COMMAND" | grep -qE 'git\s+push\b' && echo "$COMMAND" | grep -qE '(-f|--force|--force-with-lease)\b'; then
|
|
42
|
+
echo "BLOCKED: Force push is not allowed in the pipeline." >&2
|
|
43
|
+
exit 2
|
|
44
|
+
fi
|
|
45
|
+
|
|
46
|
+
# Block git remote manipulation (prevents adding attacker-controlled remotes)
|
|
47
|
+
if echo "$COMMAND" | grep -qE 'git\s+remote\s+(add|set-url)\b'; then
|
|
48
|
+
echo "BLOCKED: Modifying git remotes is not allowed in the pipeline." >&2
|
|
49
|
+
exit 2
|
|
50
|
+
fi
|
|
51
|
+
|
|
52
|
+
# Block git alias creation (prevents aliasing blocked commands)
|
|
53
|
+
if echo "$COMMAND" | grep -qE 'git\s+config\s+.*alias\.'; then
|
|
54
|
+
echo "BLOCKED: Creating git aliases is not allowed in the pipeline." >&2
|
|
55
|
+
exit 2
|
|
56
|
+
fi
|
|
57
|
+
|
|
58
|
+
# ---------------------------------------------------------------------------
|
|
59
|
+
# Settings / config tampering
|
|
60
|
+
# ---------------------------------------------------------------------------
|
|
61
|
+
|
|
62
|
+
# Block writes to settings.json (hooks, permissions, allowlists)
|
|
63
|
+
if echo "$COMMAND" | grep -qE '(>|>>|tee|mv|cp|chmod|chattr|rm).*\.claude/(settings|settings\.local)\.json'; then
|
|
64
|
+
echo "BLOCKED: Modifying Claude settings is not allowed in the pipeline." >&2
|
|
65
|
+
exit 2
|
|
66
|
+
fi
|
|
67
|
+
|
|
68
|
+
# Block git hooks path changes
|
|
69
|
+
if echo "$COMMAND" | grep -qE 'git\s+config.*(core\.hooksPath|hooks)'; then
|
|
70
|
+
echo "BLOCKED: Modifying git hooks configuration is not allowed." >&2
|
|
71
|
+
exit 2
|
|
72
|
+
fi
|
|
73
|
+
|
|
74
|
+
# ---------------------------------------------------------------------------
|
|
75
|
+
# Secret exfiltration prevention
|
|
76
|
+
# ---------------------------------------------------------------------------
|
|
77
|
+
SECRET_VARS='GITHUB_TOKEN|ANTHROPIC_API_KEY|CLAUDE_CODE_OAUTH_TOKEN|SUPABASE_ACCESS_TOKEN|TELEGRAM_BOT_TOKEN|OPENAI_API_KEY'
|
|
78
|
+
OUTBOUND_TOOLS='curl|wget|nc|ncat|netcat|python.*http|node.*http|fetch|gh\s+api'
|
|
79
|
+
|
|
80
|
+
# Check if command uses an outbound tool AND references a secret env var
|
|
81
|
+
if echo "$COMMAND" | grep -qE "(${OUTBOUND_TOOLS})\b"; then
|
|
82
|
+
if echo "$COMMAND" | grep -qE "\\\$(${SECRET_VARS})|\\$\{(${SECRET_VARS})"; then
|
|
83
|
+
echo "BLOCKED: Outbound commands cannot reference secret environment variables." >&2
|
|
84
|
+
exit 2
|
|
85
|
+
fi
|
|
86
|
+
fi
|
|
87
|
+
|
|
88
|
+
# Block piping env/printenv/set output to outbound commands
|
|
89
|
+
if echo "$COMMAND" | grep -qE '(env|printenv|set)\s*\|.*(curl|wget|nc|netcat|gh)'; then
|
|
90
|
+
echo "BLOCKED: Cannot pipe environment variables to outbound commands." >&2
|
|
91
|
+
exit 2
|
|
92
|
+
fi
|
|
93
|
+
|
|
94
|
+
# Block specific printenv calls for known secrets piped to outbound commands
|
|
95
|
+
if echo "$COMMAND" | grep -qE "printenv\s+(${SECRET_VARS})"; then
|
|
96
|
+
if echo "$COMMAND" | grep -qE "(${OUTBOUND_TOOLS})\b"; then
|
|
97
|
+
echo "BLOCKED: Cannot read secret env vars in outbound command context." >&2
|
|
98
|
+
exit 2
|
|
99
|
+
fi
|
|
100
|
+
fi
|
|
101
|
+
|
|
102
|
+
# Block reading .env files and piping to outbound commands
|
|
103
|
+
if echo "$COMMAND" | grep -qE 'cat.*\.env.*\|.*(curl|wget|nc|gh)'; then
|
|
104
|
+
echo "BLOCKED: Cannot pipe .env contents to outbound commands." >&2
|
|
105
|
+
exit 2
|
|
106
|
+
fi
|
|
107
|
+
|
|
108
|
+
# Block /proc/self/environ reads in outbound contexts
|
|
109
|
+
if echo "$COMMAND" | grep -qE '/proc/(self|1)/environ'; then
|
|
110
|
+
if echo "$COMMAND" | grep -qE "(${OUTBOUND_TOOLS}|base64)\b"; then
|
|
111
|
+
echo "BLOCKED: Cannot read /proc/environ in outbound command context." >&2
|
|
112
|
+
exit 2
|
|
113
|
+
fi
|
|
114
|
+
fi
|
|
115
|
+
|
|
116
|
+
exit 0
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# log-commands.sh
|
|
3
|
+
# PostToolUse hook — fires after every Bash command.
|
|
4
|
+
# Appends a timestamped log of every command the agent ran.
|
|
5
|
+
# Sanitizes known secrets to prevent accidental leakage.
|
|
6
|
+
#
|
|
7
|
+
# Logs go to the Daytona volume at ~/.claude/logs/ so they persist
|
|
8
|
+
# across ephemeral sandboxes for debugging and audit.
|
|
9
|
+
|
|
10
|
+
INPUT=$(cat)
|
|
11
|
+
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // ""')
|
|
12
|
+
EXIT_CODE=$(echo "$INPUT" | jq -r '.tool_response.exit_code // "?"')
|
|
13
|
+
PRD_ID="${PRD_ID:-unknown}"
|
|
14
|
+
|
|
15
|
+
SANDBOX_HOME="${SANDBOX_HOME:-/home/daytona}"
|
|
16
|
+
mkdir -p "${SANDBOX_HOME}/.claude/logs"
|
|
17
|
+
|
|
18
|
+
# Sanitize secrets before logging — redact known env vars and common patterns
|
|
19
|
+
SANITIZED="$COMMAND"
|
|
20
|
+
if [[ -n "${GITHUB_TOKEN:-}" ]]; then
|
|
21
|
+
SANITIZED="${SANITIZED//$GITHUB_TOKEN/[REDACTED]}"
|
|
22
|
+
fi
|
|
23
|
+
if [[ -n "${TELEGRAM_BOT_TOKEN:-}" ]]; then
|
|
24
|
+
SANITIZED="${SANITIZED//$TELEGRAM_BOT_TOKEN/[REDACTED]}"
|
|
25
|
+
fi
|
|
26
|
+
if [[ -n "${ANTHROPIC_API_KEY:-}" ]]; then
|
|
27
|
+
SANITIZED="${SANITIZED//$ANTHROPIC_API_KEY/[REDACTED]}"
|
|
28
|
+
fi
|
|
29
|
+
if [[ -n "${CLAUDE_CODE_OAUTH_TOKEN:-}" ]]; then
|
|
30
|
+
SANITIZED="${SANITIZED//$CLAUDE_CODE_OAUTH_TOKEN/[REDACTED]}"
|
|
31
|
+
fi
|
|
32
|
+
if [[ -n "${SUPABASE_ACCESS_TOKEN:-}" ]]; then
|
|
33
|
+
SANITIZED="${SANITIZED//$SUPABASE_ACCESS_TOKEN/[REDACTED]}"
|
|
34
|
+
fi
|
|
35
|
+
if [[ -n "${OPENAI_API_KEY:-}" ]]; then
|
|
36
|
+
SANITIZED="${SANITIZED//$OPENAI_API_KEY/[REDACTED]}"
|
|
37
|
+
fi
|
|
38
|
+
# Catch common token patterns that might not be in env vars
|
|
39
|
+
SANITIZED=$(echo "$SANITIZED" | sed \
|
|
40
|
+
-e 's/ghp_[A-Za-z0-9_]\{36,\}/[REDACTED]/g' \
|
|
41
|
+
-e 's/ghs_[A-Za-z0-9_]\{36,\}/[REDACTED]/g' \
|
|
42
|
+
-e 's/Bearer [^ ]*/Bearer [REDACTED]/g' \
|
|
43
|
+
-e 's/sk-[A-Za-z0-9_-]\{20,\}/[REDACTED]/g')
|
|
44
|
+
|
|
45
|
+
echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] [${PRD_ID}] [exit:${EXIT_CODE}] ${SANITIZED}" \
|
|
46
|
+
>> "${SANDBOX_HOME}/.claude/logs/bash-commands.log"
|
|
47
|
+
|
|
48
|
+
exit 0
|