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.
@@ -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