Repository navigation
Track denied Bash commands for later inspection (#73) - #267
zwanski-start wants to merge 3 commits into
Conversation
The security hook now appends every rejected command (name, full command, reason, denial kind, timestamp) to a bounded per-project JSONL log, and security.py exposes helpers to load, summarize and clear it. Refs AutoForgeAI#73 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
GET /api/projects/{name}/denied-commands returns denials grouped by command
(with counts, latest example, denial kind and whether the command has since
been allowed). DELETE clears the log.
Refs AutoForgeAI#73
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Adds a shield button (with count) to the control bar that opens a dialog listing commands blocked by the security hook, grouped with counts and the latest example. Users can copy a ready-made allowed_commands.yaml snippet for allowable commands or clear the log. Closes AutoForgeAI#73 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
🔍 QA report: ✅ PassedTester: Ken (cubefarm agent, in a fresh session) · Round: 1 · Author: Barbara Bash commands blocked by the security hook are now written to .autoforge/denied_commands.jsonl and shown in a new Denied Commands dialog with a count badge, which is what issue #73 asks for. A comparison against master shows the security hook's allow/block decisions are byte-identical; the change only adds logging, and a logging failure still blocks the command. The API, the 'Now allowed' detection, the allowlist YAML copy, Clear and the phone layout all worked in a real browser with no console errors. Ruff, the security tests and the UI build/lint pass; the pytest failures and mypy errors are the same ones already on master. The UI e2e suite could not run because the Playwright browser download stalled on this machine.
🧪 Commands run
No browser screenshots were taken in this round. Posted by cubefarm · merges automatically once GitHub's checks pass |
Every Bash command the hook refuses is appended to .autoforge/denied_commands.jsonl with the command, the kind of refusal (not allowed / blocklisted / failed a check / unparseable), the full command and the reason. A shield button in the control bar lists them grouped by command, flags those since added to the allowlist, and copies an allowlist snippet for review. Adaptations on integration: - written on top of the current security.py (nested-command parser, allowlist and features.db protection) instead of AutoForgeAI#267's refactor: block results carry the refusal kind internally and validate_bash_tool_use strips it before answering the CLI, then records the entry; - the hook runs outside the agent's sandbox and logs text the agent chose, so the log is appended, read and deleted with api/safe_project_io (no planted symlink or hard link is followed; the API answers 409 for one); - rotation by an atomic rename to denied_commands.jsonl.1 (from AutoForgeAI#322) instead of rewriting the file; readers read .1 then the current file; - the log and its .1 are read-only for the agent (sandbox denyWrite + Edit deny), so it cannot forge entries that would talk the user into widening the allowlist; - duplicate project-dir helper dropped (one already exists in projects.py). Integrates AutoForgeAI#267 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AeH26pcsqoxPb3CDs8g1VN
What changed
Every Bash command blocked by the security hook is now recorded, so users can review them later and decide what to allow.
Backend
security.py:bash_security_hooknow writes each rejected command to.autoforge/denied_commands.jsonl. Each entry has a timestamp, the denied command name, the full command, the reason, and the kind of denial:not_allowed,blocklisted,validationorparse_error._evaluate_bash_command(). The allow/block decisions are unchanged.load_denied_commands,summarize_denied_commands(groups entries by command and flagscurrently_allowedif the command has since been allowed) andclear_denied_commands.autoforge_paths.py: newget_denied_commands_path(). The log is added to the.autoforge/.gitignoretemplate.server/routers/projects.py: newGET /api/projects/{name}/denied-commands(grouped, most frequent first) andDELETE /api/projects/{name}/denied-commands.UI
allowed_commands.yamlsnippet with the commands that a project can allow.How I verified it
python test_security.py: all pass, including the newtest_denied_command_loggingwith 17 checks.python test_security_integration.py: passes.test_denied_commands_api.py(5 pytest tests for the endpoints, including 404 and 400): passes.ruff check .is clean.mypy .reports no new errors (the 25 errors already on master remain).npm run lintshows no new warnings andnpm run buildsucceeds.chmod 777, python). The badge showed 5, the dialog grouped them correctly, and Clear emptied the log.Assumptions
project_dir, which is always the case for agent sessions throughclient.py. Calls without a project directory, as in some tests, are not logged.zwanski-startfork because this account can't push to the upstream repo.Closes #73
🤖 Generated with Claude Code