Repository navigation
Record denied Bash commands for later inspection - #322
zwanski-start wants to merge 1 commit into
Conversation
The security hook now appends every blocked Bash command to
.autoforge/denied_commands.jsonl (timestamp, offending command, full
command line, reason) so users can see which tools the agent wanted and
extend their allowlists. The log is gitignored and rotates at ~1 MB.
Adds GET/DELETE /api/projects/{name}/denied-commands to view the log
aggregated by command and to clear it, plus tests and docs.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
🔍 QA report: ✅ PassedTester: Guido (cubefarm agent, in a fresh session) · Round: 1 · Author: Dennis The PR records every Bash command the security hook blocks to .autoforge/denied_commands.jsonl and adds GET/DELETE /api/projects/{name}/denied-commands, which meets issue #73. The allow/block logic is unchanged: all security unit and integration tests pass, and recording errors never change the decision. Tested against a real uvicorn server with an isolated registry: aggregation, recent entries, clearing, 404/400 errors, rotation and skipping of non-JSON lines all work. Only minor non-blocking nits found: the GET returns 500 if a hand-edited log line is valid JSON with wrong field types, and the per-command name field is not truncated.
🧪 Commands run
No browser screenshots were taken in this round. Posted by cubefarm · merges automatically once GitHub's checks pass |
Closes #73
What changed
security.py:bash_security_hooknow records every blocked Bash command (not on the allowlist, on the hard/org blocklist, failed extra validation for pkill/chmod/init.sh/playwright-cli, or couldn't be parsed) to{project}/.autoforge/denied_commands.jsonl. Each line hastimestamp,command(the specific command that caused the block, e.g.pytest),full_command(truncated to 2000 chars) andreason(first line). The validation logic moved unchanged into_validate_bash_command, which also returns the offending command name. Recording errors are logged and ignored, so they never change the allow/block decision.record_denied_command,load_denied_commands,summarize_denied_commands(aggregates by command: count, last seen, most recent example),clear_denied_commands.autoforge_paths.py:get_denied_commands_log_path(), anddenied_commands.jsonl/.1added to the generated.autoforge/.gitignore.GET /api/projects/{name}/denied-commandsreturns the aggregated summary and the 100 most recent entries.DELETEon the same URL clears the log.examples/README.md(includes ajqone-liner), plus CLAUDE.md updates.Verification
python test_security.py: 182 passed, including 13 new checks intest_denied_commands_loggingcovering recording, aggregation, no-project-dir, malformed lines, rotation and clearing.python test_security_integration.py: all passed.ruff check .is clean.mypy .reports no new errors (24 before and after, none in the changed files).pytest test_client.py test_ws_security.py test_devserver_security.pygives the same 21 failures on unmodified master, so they are not caused by this change.pytest -x,openssl genrsa 2048,pytestandls, then called the endpoints through FastAPI's TestClient. GET returnedpytest×2 andopenssl×1 (lswas not logged), DELETE cleared the log, and an unknown project returned 404.Assumptions / notes
cat/jq. Parallel agents append single short lines, which is safe enough on POSIX..1) above about 1 MB, so it can't grow without limit.full_commandis stored as-is. It could contain secrets the agent typed (for example a token in a curl command). It stays in the project's gitignored.autoforge/directory, the same trust level as the rest of the agent's local state.🤖 Generated with Claude Code