diff --git a/plugins/ultraplan-local/agents/session-decomposer.md b/plugins/ultraplan-local/agents/session-decomposer.md index 5a2fc80..ce0d7d3 100644 --- a/plugins/ultraplan-local/agents/session-decomposer.md +++ b/plugins/ultraplan-local/agents/session-decomposer.md @@ -185,8 +185,9 @@ The script must: **Important script conventions:** - Use `#!/usr/bin/env bash` shebang - Use `set -euo pipefail` -- Each `claude -p` invocation must use `--dangerously-skip-permissions`. Prepend - `unset ANTHROPIC_API_KEY` before each invocation to prevent accidental API billing +- Each `claude -p` invocation must use `--allowedTools "Read,Write,Edit,Bash,Glob,Grep"` + and `--permission-mode bypassPermissions`. Prepend `unset ANTHROPIC_API_KEY` + before each invocation to prevent accidental API billing - Background processes use `&` and are collected with `wait` - PID tracking for wait targets - Exit codes propagated correctly diff --git a/plugins/ultraplan-local/commands/ultraexecute-local.md b/plugins/ultraplan-local/commands/ultraexecute-local.md index 6aeb916..8c2d3d9 100644 --- a/plugins/ultraplan-local/commands/ultraexecute-local.md +++ b/plugins/ultraplan-local/commands/ultraexecute-local.md @@ -115,6 +115,82 @@ Steps: {N} {if warnings}: Warnings: {list} ``` +## Phase 2.4 — Pre-execution security scan + +**Runs for all modes except dry-run** (dry-run has its own report format). + +Scan every `Verify:` and `Checkpoint:` command in the parsed plan against the +executor security denylist. This catches dangerous commands before execution begins. + +### Extract commands + +For each step in the plan, extract the command string from: +- `Verify:` field (the shell command after the backtick-quoted command) +- `Checkpoint:` field (the git commit command) + +Also extract Exit Condition commands if present. + +### Check against denylist + +For each extracted command, check against these patterns: + +**BLOCK patterns (stop execution immediately):** + +| Pattern | Threat | +|---------|--------| +| `rm` with both `-r` and `-f` flags (any order) | Recursive force delete | +| `chmod 777` or `chmod -R 777` | World-writable permissions | +| `curl`/`wget` piped to `bash`/`sh`/`zsh` | Remote code execution | +| `eval` with `$`, backtick, or `$(` | Code injection via eval | +| `mkfs` or `dd` writing to `/dev/sd*`, `/dev/nvme*`, `/dev/hd*` | Disk destruction | +| `shutdown`, `reboot`, `halt`, `poweroff` | System shutdown | +| `:(){ :\|:& };:` pattern | Fork bomb | +| `base64` piped to `bash`/`sh` | Obfuscated code execution | +| `crontab -e` or writing to `/etc/cron*` | Persistence via cron | +| `kill -9 -1` or `pkill -9 -1` | Kill all user processes | +| `history -c` or truncating `~/.bash_history` | Evidence destruction | + +**WARN patterns (report but continue):** + +| Pattern | Concern | +|---------|---------| +| `npm install --save`, `pip install`, `cargo add` | Dependency changes during execution | +| `git push --force` | History rewrite | +| `git reset --hard` | Discard uncommitted changes | + +### Scan output + +For each match: +``` +Security scan: Step {N} — {description} + Command: {command} + {BLOCKED | WARNING}: {pattern name} +``` + +**If ANY BLOCK pattern is found:** + +``` +SECURITY SCAN FAILED: {count} dangerous command(s) found in plan. + +Blocked commands: + Step {N}: {command} → {reason} + +This plan contains commands blocked by the executor security policy. +The plan may have been tampered with or contain hallucinated dangerous commands. + +Options: + 1. Review and fix the plan file: {path} + 2. Use --dry-run to inspect all commands without executing + 3. Use --fg for interactive execution (hooks provide additional protection) +``` + +Stop execution. Do NOT continue to Phase 2.5. + +**If only WARN patterns found:** Continue execution but include warnings in the +pre-execution summary. Report them in the final output under "Security advisories." + +**If clean:** Report `Security scan: PASS ({N} commands checked)` and continue. + ## Phase 2.5 — Execution strategy decision Determine how to execute this plan: @@ -321,7 +397,8 @@ Worktree created: session-{N} → {WORKTREE_PATH} (branch: {BRANCH_NAME}) For each session N in the wave: ```bash cd "$WORKTREE_PATH" && claude -p "/ultraexecute-local --session {N} {plan-path}" \ - --dangerously-skip-permissions \ + --allowedTools "Read,Write,Edit,Bash,Glob,Grep" \ + --permission-mode bypassPermissions \ > "$LOG_DIR/session-{N}.log" 2>&1 & ``` @@ -646,6 +723,31 @@ Read the step's `Files:` and `Changes:` fields. Implement exactly as described. #### Sub-step D — Verification +**Security check (mandatory):** Before running the Verify command, check it against +the executor security denylist. If the command matches ANY of these patterns, +**refuse to execute** — treat as `On failure: escalate` regardless of the plan's +On failure setting: + +- `rm -rf` or `rm -fr` with any path +- `chmod 777` or `chmod -R 777` +- Pipe-to-shell: `curl ... | bash`, `wget ... | sh`, `base64 ... | bash` +- `eval` with variable expansion: `eval $VAR`, `eval $(cmd)`, `` eval `cmd` `` +- `mkfs`, `dd` writing to block devices (`/dev/sd*`, `/dev/nvme*`) +- `shutdown`, `reboot`, `halt`, `poweroff` +- Fork bomb patterns +- `crontab` writes, `/etc/cron*` modifications +- `kill -9 -1` or `pkill -9 -1` (kill all processes) +- `history -c` or truncating `~/.bash_history` + +If matched: +1. Do NOT execute the command +2. Set step status = "failed" +3. Log: `SECURITY: Verify command blocked — matches executor denylist: {pattern name}` +4. Apply `On failure: escalate` regardless of the plan's On failure setting +5. Include in final report under a "Security blocks" section + +If the command passes the security check, run it: + Run the `Verify:` command exactly as written, via Bash. **Rules:** @@ -877,3 +979,19 @@ Never let stats failures block the workflow. `--no-ff`. If any merge produces a conflict, run `git merge --abort`, report the conflicting files, and do not attempt further merges. Never use `--force` or `--strategy-option theirs/ours` to silently resolve conflicts. + +14. **Verify command security check.** Before executing any `Verify:` or + `Checkpoint:` command, check it against the executor security denylist + (Sub-step D). If the command matches a blocked pattern, escalate + immediately — do not execute, do not retry. + +15. **No writing outside the repository.** During step execution, never write + files outside the git repository root (`git rev-parse --show-toplevel`). + Exception: `.claude/` paths for plans, progress files, and stats. + This prevents escape-from-repo attacks where a plan step modifies home + directory or system files. + +16. **No writing to security-sensitive paths.** Never write to `.git/hooks/` + (git hook injection), `~/.ssh/`, `~/.aws/`, `~/.gnupg/`, `.env` files, + shell configs (`~/.zshrc`, `~/.bashrc`, `~/.profile`), or + `.claude/settings.json` / `.claude/hooks/` (infrastructure self-modification). diff --git a/plugins/ultraplan-local/hooks/hooks.json b/plugins/ultraplan-local/hooks/hooks.json new file mode 100644 index 0000000..c6aaded --- /dev/null +++ b/plugins/ultraplan-local/hooks/hooks.json @@ -0,0 +1,24 @@ +{ + "hooks": { + "PreToolUse": [ + { + "matcher": "Bash", + "hooks": [ + { + "type": "command", + "command": "node ${CLAUDE_PLUGIN_ROOT}/hooks/scripts/pre-bash-executor.mjs" + } + ] + }, + { + "matcher": "Write", + "hooks": [ + { + "type": "command", + "command": "node ${CLAUDE_PLUGIN_ROOT}/hooks/scripts/pre-write-executor.mjs" + } + ] + } + ] + } +} diff --git a/plugins/ultraplan-local/hooks/scripts/pre-bash-executor.mjs b/plugins/ultraplan-local/hooks/scripts/pre-bash-executor.mjs new file mode 100644 index 0000000..abe75d3 --- /dev/null +++ b/plugins/ultraplan-local/hooks/scripts/pre-bash-executor.mjs @@ -0,0 +1,247 @@ +#!/usr/bin/env node +// Hook: pre-bash-executor.mjs +// Event: PreToolUse (Bash) +// Purpose: Block or warn about destructive shell commands during plan execution. +// +// Protocol: +// - Read JSON from stdin: { tool_name, tool_input } +// - tool_input.command — the shell command string +// - BLOCK (exit 2): catastrophic/irreversible operations +// - WARN (exit 0): risky but recoverable operations — advisory to stderr +// - Allow (exit 0): everything else +// +// Based on llm-security's pre-bash-destructive.mjs with executor-specific additions. +// bash-normalize logic copied inline (MIT) — cannot import from separate plugin. + +import { readFileSync } from 'node:fs'; + +// --------------------------------------------------------------------------- +// Bash normalization (from llm-security/scanners/lib/bash-normalize.mjs) +// Strips bash evasion techniques: empty quotes, ${} expansion, backslash splitting. +// --------------------------------------------------------------------------- +function normalizeBashExpansion(cmd) { + if (!cmd || typeof cmd !== 'string') return cmd || ''; + + let result = cmd + // Strip empty single quotes: w''get -> wget + .replace(/''/g, '') + // Strip empty double quotes: r""m -> rm + .replace(/""/g, '') + // Single-char ${x} -> x (evasion: c${u}rl -> curl, assumes x=x) + .replace(/\$\{(\w)\}/g, '$1') + // Multi-char ${ANYTHING} -> '' (unknown value, strip entirely) + .replace(/\$\{[^}]*\}/g, '') + // Strip backtick subshell with empty/whitespace content + .replace(/`\s*`/g, ''); + + // Iteratively strip backslash between word chars (c\u\r\l needs 2 passes) + let prev; + do { + prev = result; + result = result.replace(/(\w)\\(\w)/g, '$1$2'); + } while (result !== prev); + + return result; +} + +// --------------------------------------------------------------------------- +// BLOCK rules — exit 2, command is not executed. +// --------------------------------------------------------------------------- +const BLOCK_RULES = [ + { + name: 'Filesystem root/home destruction (rm -rf /)', + // Matches rm with both -r and -f flags targeting /, ~, or $HOME. + // Uses (?:\s|$) instead of \b because / and ~ are non-word chars. + pattern: /\brm\s+(?:-[a-zA-Z]*f[a-zA-Z]*\s+|--force\s+)*-[a-zA-Z]*r[a-zA-Z]*\s+(?:\/|~|\$HOME)(?:\s|$)/, + description: + '`rm -rf /`, `rm -rf ~`, and `rm -rf $HOME` would destroy the filesystem ' + + 'or home directory. Unconditionally blocked.', + }, + { + name: 'World-writable chmod (chmod 777)', + pattern: /\bchmod\s+(?:-[a-zA-Z]+\s+)*777\b/, + description: + '`chmod 777` grants full read/write/execute to all users. ' + + 'Use minimal permissions (e.g. 644, 755).', + }, + { + name: 'Pipe-to-shell (curl|bash, wget|sh)', + pattern: /(?:curl|wget)\b[^|]*\|\s*(?:bash|sh|zsh|ksh|dash)\b/, + description: + 'Piping remote content into a shell allows arbitrary remote code execution. ' + + 'Download first, review, then execute.', + }, + { + name: 'Fork bomb', + pattern: /:\(\)\s*\{\s*:\s*\|\s*:&\s*\}\s*;?\s*:/, + description: 'Fork bomb — exhausts system process resources. Blocked.', + }, + { + name: 'Filesystem format (mkfs)', + pattern: /\bmkfs(?:\.[a-z0-9]+)?\s/, + description: '`mkfs` formats a filesystem, destroying all data. Blocked.', + }, + { + name: 'Raw disk overwrite via dd', + pattern: /\bdd\b[^&|;]*\bof=\/dev\/(?:sd|nvme|hd|vd|xvd|mmcblk)[a-z0-9]*/, + description: '`dd` writing to a raw block device destroys disk data. Blocked.', + }, + { + name: 'Direct device write (> /dev/sd*)', + pattern: />\s*\/dev\/(?:sd|nvme|hd|vd|xvd|mmcblk)[a-z0-9]*/, + description: 'Shell redirection to a block device destroys disk data. Blocked.', + }, + { + name: 'eval with variable/command expansion', + pattern: /\beval\s+(?:`|\$[\({]|"[^"]*\$)/, + description: + '`eval` with variable or command substitution is a code injection vector. ' + + 'Refactor to use explicit commands.', + }, + // --- Executor-specific additions --- + { + name: 'System shutdown/reboot', + pattern: /\b(?:shutdown|reboot|halt|poweroff)\b/, + description: 'System shutdown/reboot commands are blocked during execution.', + }, + { + name: 'Cron persistence', + pattern: /\bcrontab\b|>\s*\/etc\/cron/, + description: + 'Writing to crontab or /etc/cron* creates persistent scheduled tasks. ' + + 'Blocked during execution.', + }, + { + name: 'Base64-encoded execution', + pattern: /\bbase64\b[^|]*\|\s*(?:bash|sh|zsh)\b/, + description: 'Base64-decoded content piped to shell is obfuscated code execution. Blocked.', + }, + { + name: 'Kill all processes (kill -9 -1)', + pattern: /\b(?:kill|pkill)\s+-9\s+-1\b/, + description: 'Killing all user processes with signal 9. Blocked.', + }, + { + name: 'History destruction', + pattern: /\bhistory\s+-c\b|>\s*~\/\.bash_history\b|>\s*~\/\.zsh_history\b/, + description: 'Clearing shell history or truncating history files. Blocked.', + }, +]; + +// --------------------------------------------------------------------------- +// WARN rules — exit 0 with advisory message on stderr. +// --------------------------------------------------------------------------- +const WARN_RULES = [ + { + name: 'Force push (git push --force)', + pattern: /\bgit\s+push\b[^|&;]*(?:--force|-f)\b/, + description: + 'WARNING: `git push --force` rewrites remote history. Prefer `--force-with-lease`.', + }, + { + name: 'Hard reset (git reset --hard)', + pattern: /\bgit\s+reset\s+--hard\b/, + description: + 'WARNING: `git reset --hard` permanently discards uncommitted changes.', + }, + { + name: 'Recursive remove (rm -rf, non-root)', + pattern: /\brm\s+(?:-[a-zA-Z]*f[a-zA-Z]*\s+|--force\s+)*-[a-zA-Z]*r[a-zA-Z]*\s+/, + description: + 'WARNING: `rm -rf` permanently deletes files. Verify the target path.', + }, + { + name: 'Docker system prune', + pattern: /\bdocker\s+system\s+prune\b/, + description: + 'WARNING: `docker system prune` removes all stopped containers and unused images.', + }, + { + name: 'npm publish', + pattern: /\bnpm\s+publish\b/, + description: + 'WARNING: `npm publish` releases a package to the public registry.', + }, + { + name: 'DROP TABLE or DROP DATABASE (SQL)', + pattern: /\bDROP\s+(?:TABLE|DATABASE|SCHEMA)\b/i, + description: + 'WARNING: SQL DROP permanently deletes database objects.', + }, + { + name: 'DELETE without WHERE (SQL)', + pattern: /\bDELETE\s+FROM\s+\w+(?:\s*;|\s*$)/i, + description: + 'WARNING: DELETE FROM without WHERE deletes all rows.', + }, + // --- Executor-specific additions --- + { + name: 'Dependency installation during execution', + pattern: /\b(?:npm\s+install\s+--save|pip3?\s+install\s+(?!-e\s+\.)|cargo\s+add)\b/, + description: + 'WARNING: Installing dependencies during plan execution is unusual. ' + + 'Verify this is intentional.', + }, +]; + +// --------------------------------------------------------------------------- +// Normalize: strip ANSI, collapse whitespace +// --------------------------------------------------------------------------- +function normalizeCommand(cmd) { + return cmd + .replace(/\x1B\[[0-9;]*m/g, '') + .replace(/\s+/g, ' ') + .trim(); +} + +// --------------------------------------------------------------------------- +// Main +// --------------------------------------------------------------------------- +let input; +try { + const raw = readFileSync(0, 'utf-8'); + input = JSON.parse(raw); +} catch { + // Cannot parse stdin — fail open. + process.exit(0); +} + +const command = input?.tool_input?.command; + +if (!command || typeof command !== 'string') { + process.exit(0); +} + +// Strip bash evasion, then normalize whitespace +const deobfuscated = normalizeBashExpansion(command); +const normalized = normalizeCommand(deobfuscated); + +// Check BLOCK rules first +for (const rule of BLOCK_RULES) { + if (rule.pattern.test(normalized)) { + process.stderr.write( + `[ultraplan] BLOCKED: ${rule.name}\n` + + ` Command: ${normalized.slice(0, 200)}${normalized.length > 200 ? '...' : ''}\n` + + ` ${rule.description}\n` + ); + process.exit(2); + } +} + +// Check WARN rules (advisory — still exit 0) +const warnings = []; +for (const rule of WARN_RULES) { + if (rule.pattern.test(normalized)) { + warnings.push(` [WARN] ${rule.name}: ${rule.description}`); + } +} + +if (warnings.length > 0) { + process.stderr.write( + `[ultraplan] SECURITY ADVISORY: Potentially risky command.\n` + + ` Command: ${normalized.slice(0, 200)}${normalized.length > 200 ? '...' : ''}\n` + + warnings.join('\n') + '\n' + ); +} + +process.exit(0); diff --git a/plugins/ultraplan-local/hooks/scripts/pre-write-executor.mjs b/plugins/ultraplan-local/hooks/scripts/pre-write-executor.mjs new file mode 100644 index 0000000..020aa91 --- /dev/null +++ b/plugins/ultraplan-local/hooks/scripts/pre-write-executor.mjs @@ -0,0 +1,125 @@ +#!/usr/bin/env node +// Hook: pre-write-executor.mjs +// Event: PreToolUse (Write) +// Purpose: Block writes to security-sensitive paths during plan execution. +// +// Protocol: +// - Read JSON from stdin: { tool_name, tool_input } +// - tool_input.file_path — the target path for Write tool +// - BLOCK (exit 2): writes to security infrastructure, shell configs, secrets +// - Allow (exit 0): everything else + +import { readFileSync } from 'node:fs'; +import { resolve } from 'node:path'; + +const HOME = process.env.HOME || process.env.USERPROFILE || '/tmp'; + +// --------------------------------------------------------------------------- +// BLOCK rules — path patterns that must never be written during execution. +// --------------------------------------------------------------------------- +const BLOCK_RULES = [ + { + name: 'Git hook injection (.git/hooks/)', + test: (p) => /\/\.git\/hooks\//.test(p), + description: + 'Writing to .git/hooks/ could inject malicious git hooks that execute ' + + 'on every commit, push, or checkout. Blocked.', + }, + { + name: 'Claude settings self-modification', + test: (p) => /\/\.claude\/settings[^/]*\.json$/.test(p), + description: + 'Writing to .claude/settings.json could disable security hooks or ' + + 'change permission modes. Blocked.', + }, + { + name: 'Claude hooks self-modification', + test: (p) => /\/\.claude\/hooks\//.test(p) || /\/\.claude-plugin\//.test(p), + description: + 'Writing to .claude/hooks/ or .claude-plugin/ could modify security ' + + 'hook configuration. Blocked.', + }, + { + name: 'Shell configuration files', + test: (p) => { + const sensitive = [ + `${HOME}/.zshrc`, + `${HOME}/.bashrc`, + `${HOME}/.bash_profile`, + `${HOME}/.profile`, + `${HOME}/.zshenv`, + `${HOME}/.zprofile`, + ]; + const resolved = resolve(p); + return sensitive.some((s) => resolved === s || resolved.startsWith(s + '.')); + }, + description: + 'Writing to shell config files (~/.zshrc, ~/.bashrc, etc.) could inject ' + + 'persistent commands. Blocked.', + }, + { + name: 'SSH directory', + test: (p) => { + const resolved = resolve(p); + return resolved.startsWith(`${HOME}/.ssh/`) || resolved === `${HOME}/.ssh`; + }, + description: 'Writing to ~/.ssh/ could compromise SSH keys or config. Blocked.', + }, + { + name: 'AWS credentials', + test: (p) => { + const resolved = resolve(p); + return resolved.startsWith(`${HOME}/.aws/`) || resolved === `${HOME}/.aws`; + }, + description: 'Writing to ~/.aws/ could compromise cloud credentials. Blocked.', + }, + { + name: 'GnuPG directory', + test: (p) => { + const resolved = resolve(p); + return resolved.startsWith(`${HOME}/.gnupg/`) || resolved === `${HOME}/.gnupg`; + }, + description: 'Writing to ~/.gnupg/ could compromise GPG keys. Blocked.', + }, + { + name: 'Environment files (.env)', + test: (p) => /\/\.env(?:\.[a-zA-Z0-9]+)?$/.test(p), + description: + 'Writing to .env files could expose or modify secrets. Blocked. ' + + 'Use .env.template instead.', + }, +]; + +// --------------------------------------------------------------------------- +// Main +// --------------------------------------------------------------------------- +let input; +try { + const raw = readFileSync(0, 'utf-8'); + input = JSON.parse(raw); +} catch { + // Cannot parse stdin — fail open. + process.exit(0); +} + +const filePath = input?.tool_input?.file_path; + +if (!filePath || typeof filePath !== 'string') { + process.exit(0); +} + +const resolved = resolve(filePath); + +for (const rule of BLOCK_RULES) { + if (rule.test(resolved)) { + process.stderr.write( + `[ultraplan] BLOCKED: ${rule.name}\n` + + ` Path: ${resolved}\n` + + ` ${rule.description}\n` + ); + process.exit(2); + } +} + +// Allow +process.exit(0); diff --git a/plugins/ultraplan-local/templates/headless-launch-template.md b/plugins/ultraplan-local/templates/headless-launch-template.md index 6281ea2..e840ef1 100644 --- a/plugins/ultraplan-local/templates/headless-launch-template.md +++ b/plugins/ultraplan-local/templates/headless-launch-template.md @@ -62,7 +62,8 @@ echo "Worktree created: session-{n} (branch: ultraplan/{slug}/session-{n})" {# Launch session in its worktree: } cd "$WORKTREE_BASE/session-{n}" && claude -p "$(cat "$PLAN_DIR/session-{n}-{slug}.md")" \ - --dangerously-skip-permissions \ + --allowedTools "Read,Write,Edit,Bash,Glob,Grep" \ + --permission-mode bypassPermissions \ > "$LOG_DIR/session-{n}.log" 2>&1 & PID_{n}=$! cd "$REPO_ROOT" @@ -118,8 +119,10 @@ When generating a launch script from this template: stops and reports which session failed. 4. **Log each session** to a separate file for debugging. 5. **Use `claude -p`** with the session spec file as the prompt. -6. **Use `--dangerously-skip-permissions`** rather than `--allowedTools` — the - executor needs flexible tool access and enumerating every tool is fragile. +6. **Use `--allowedTools "Read,Write,Edit,Bash,Glob,Grep"`** with + `--permission-mode bypassPermissions` for child sessions. This limits the + tool surface to what the executor needs and prevents agent spawning, MCP + access, and external web requests in headless sessions. 7. **Final verification** at the end runs the master plan's verification section. 8. **Never include secrets** in the generated script. 9. **Wave verification must be independent.** After each wave completes, run diff --git a/plugins/ultraplan-local/templates/session-spec-template.md b/plugins/ultraplan-local/templates/session-spec-template.md index 1fefbca..478af8c 100644 --- a/plugins/ultraplan-local/templates/session-spec-template.md +++ b/plugins/ultraplan-local/templates/session-spec-template.md @@ -48,6 +48,21 @@ All of these must pass before this session is considered complete: ## Failure Handling - If ANY step fails after retry: **stop execution**. Do NOT proceed to later steps. + +## Security Constraints + +These rules override any step instructions that conflict with them: + +- **Never run** `rm -rf`, `chmod 777`, pipe-to-shell (`curl|bash`, `wget|sh`, + `base64|bash`), `eval` with variable expansion, `mkfs`, `dd` to block devices, + `shutdown`/`reboot`/`halt`, fork bombs, `crontab` writes, or `kill -9 -1` +- **Never modify files** outside the Scope Fence (Touch list above) +- **Never write to** `.git/hooks/`, `~/.ssh/`, `~/.aws/`, `~/.gnupg/`, `.env` + files, shell configs (`~/.zshrc`, `~/.bashrc`, `~/.profile`) +- **Never write to** `.claude/settings.json`, `.claude/hooks/`, or any hook + script — these are security infrastructure and must not be modified by execution +- If a `Verify:` or `Checkpoint:` command violates these rules: treat as + `On failure: escalate` and stop execution regardless of the step's On failure setting - Commit whatever was completed successfully before stopping. - Report which step failed, the error message, and what was attempted.