Skip to content

Track denied Bash commands for later inspection (#73) - #267

Open
zwanski-start wants to merge 3 commits into
AutoForgeAI:masterfrom
zwanski-start:swarm/issue-73-barbara
Open

zwanski-start wants to merge 3 commits into
AutoForgeAI:masterfrom
zwanski-start:swarm/issue-73-barbara

Conversation

@zwanski-start

Copy link
Copy Markdown

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_hook now 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, validation or parse_error.
    • The check logic moved into _evaluate_bash_command(). The allow/block decisions are unchanged.
    • The log has a size cap: past 1 MB it keeps only the newest 2000 entries, and long commands are cut to 2000 characters.
    • Writing the log never raises, so a logging failure can't change a security decision.
    • New helpers: load_denied_commands, summarize_denied_commands (groups entries by command and flags currently_allowed if the command has since been allowed) and clear_denied_commands.
  • autoforge_paths.py: new get_denied_commands_path(). The log is added to the .autoforge/.gitignore template.
  • server/routers/projects.py: new GET /api/projects/{name}/denied-commands (grouped, most frequent first) and DELETE /api/projects/{name}/denied-commands.

UI

  • The control bar has a new shield button that shows how many denied commands are still not allowed. While the agent runs, the count refreshes every 15 s.
  • The button opens a Denied Commands dialog. For each command it shows the denial kind, how many times it was denied, when it was last seen and the latest full command line.
    • Copy allowlist YAML copies an allowed_commands.yaml snippet with the commands that a project can allow.
    • Clear empties the log.

How I verified it

  • python test_security.py: all pass, including the new test_denied_command_logging with 17 checks.
  • python test_security_integration.py: passes.
  • New 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 lint shows no new warnings and npm run build succeeds.
  • Manual check: ran the server on port 5443 with an isolated HOME, registered a demo project and ran some commands through the hook (pytest, openssl, sudo, chmod 777, python). The badge showed 5, the dialog grouped them correctly, and Clear emptied the log.

Assumptions

  • Commands are logged per project, only when the hook gets a project_dir, which is always the case for agent sessions through client.py. Calls without a project directory, as in some tests, are not logged.
  • The log only tells users what to add. It never changes the allowlist by itself, so users still review commands before allowing them.
  • The branch is pushed from the zwanski-start fork because this account can't push to the upstream repo.

Closes #73

🤖 Generated with Claude Code

zwanski-start and others added 3 commits October 22, 2026 06:50
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>
@zwanski-start

Copy link
Copy Markdown
Author

🔍 QA report: ✅ Passed

Tester: 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.

Check Details
✅ Issue acceptance: denied commands are tracked for later inspection Ran pytest, openssl, python, wget, hexdump, sudo and chmod 777 through bash_security_hook with a project_dir. Each was written to .autoforge/denied_commands.jsonl and grouped by command with the right kind (not_allowed / blocklisted / validation) and count. The allowed command 'ls -la' was not logged.
✅ Security decisions unchanged (comparison with master) Ran 22 commands (sudo, dd, rm -rf, pkill variants, chmod, init.sh, playwright-cli run-code, chained and pipe commands, $(...), an empty string) through the hook on master and on this branch, with and without project_dir. The JSON results were byte-identical. With a project_dir, each of the 10 blocked commands produced exactly one log entry.
✅ Logging failure does not change the security decision Created a file where the .autoforge directory should be. record_denied_command logged a warning and the hook still returned block.
✅ Log size limits A 5000-character heredoc was cut to 2000 characters. After 800 large entries the file was trimmed to the newest 2000 entries (so it stayed at 800). Note: once the log passes 1 MB the whole file is rewritten on every new denial, and with long commands it can stay around 8 MB. Not blocking.
✅ REST API: GET/DELETE /api/projects/{name}/denied-commands Booted uvicorn on port 5345 with an isolated HOME and registered a demo project. GET returned the groups and total. An unknown project returned 404 on GET and DELETE; an invalid name ('bad.name') returned 400. DELETE removed the log.
✅ currently_allowed flag after the allowlist changes Wrote pytest and openssl to .autoforge/allowed_commands.yaml. The API and the dialog flagged both as 'Now allowed' and the badge went from 7 to 5.
✅ UI: badge, dialog, copy YAML, clear The shield button showed 7. The dialog listed commands with kind badges, counts, timestamps and the latest full command line. Copy allowlist YAML produced valid YAML with only the not_allowed commands, leaving out sudo and chmod. Clear emptied the list and showed the empty state.
✅ Responsive layout and console At 390x844 the dialog fits with stacked footer buttons. The Graph view still renders. No console errors or warnings during the session.
✅ Python lint, types and tests Ruff is clean. mypy reports 24 errors on both master and this branch, none in the changed files. pytest has the same 21 failures in test_devserver_security.py on master and on this branch; the new endpoint tests pass.
⏭️ UI e2e (npm run test:e2e) The Playwright chromium-headless-shell browser was missing on this machine and its download stalled at 1.6 MB for over 2 minutes, so the run was stopped. The existing specs cover the assistant panel and tooltips, not this feature; the feature was tested manually in a real browser instead.
🧪 Commands run
Command Result
venv/bin/ruff check . All checks passed
venv/bin/python test_security.py 186 passed, 0 failed
venv/bin/python test_security_integration.py 9 passed, 0 failed
venv/bin/python -m pytest -q 150 passed, 21 failed (all in test_devserver_security.py; master shows the same 21 failures with 144 passed)
venv/bin/mypy . 24 errors in 9 files, the same count as master; none in the changed files
npm --prefix ui run build tsc -b and vite build succeeded
npm --prefix ui run lint 0 errors, 6 warnings, all in files this PR doesn't touch
Script running 22 commands through bash_security_hook on master and on this branch Identical decisions; 10 log entries for 10 blocked commands
uvicorn server.main:app --port 5345 + curl GET/DELETE /api/projects/{demo,nope,bad.name}/denied-commands 200 with grouped data / 404 / 400 / 404 on DELETE of an unknown project
npx playwright install chromium-headless-shell && npx playwright test Not run: the browser download stalled, so the task was stopped

No browser screenshots were taken in this round.

Posted by cubefarm · merges automatically once GitHub's checks pass

cabana8471-arch pushed a commit to cabana8471-arch/autoforge that referenced this pull request Nov 8, 2026
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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Feature request: Gather a list of denied commands during execution for later inspection

1 participant