Tutorial 5: Running malac unattended
When nobody is at the keyboard, malac cannot ask for permission. In this tutorial you set up autopilot with deny rules as guard rails, try them locally, run malac in GitHub Actions with JSON output, and review what it did.
Write the guard rails
Section titled “Write the guard rails”-
Create a configuration for unattended runs. Permissions can only be set in the user configuration (a project file cannot widen or narrow them unless you trust it), so keep a separate malac home for these runs.
MALAC_HOMEmoves configuration, sessions and everything else:export MALAC_HOME="$HOME/.malac-ci"mkdir -p "$MALAC_HOME"cat > "$MALAC_HOME/config.toml" <<'EOF'model = "lora/default"[providers.lora]kind = "openai"base_url = "https://lora.example.com/v1"api_key_env = "LORA_API_KEY"[permissions]deny = ["bash(git push*)","bash(git remote*)","bash(sudo *)","bash(rm -rf /*)","bash(curl *)","bash(wget *)","web_fetch","computer_*","browser_*","malac_configure",][control]enabled = false[update]check = falseEOFDeny rules are checked first and hold in every mode. Every
bashcommand is split on&&,||,;,|and newlines, and a deny match on any segment blocks the whole command. -
Check the merged result.
malac config show | sed -n '/\[permissions\]/,/^$/p'[permissions]allow = []deny = ["bash(git push*)",…]
Try it locally
Section titled “Try it locally”-
Use a scratch repository. The shop project from tutorial 2 works; break a test first:
cd shopsed -i.bak 's/127.0/128.0/' test_shop.py && rm test_shop.py.bak -
Run on autopilot.
malac -y -p run the tests, fix the failure, then commit and push the fix● bash python3 -m unittest -q⎿ F..● read test_shop.py● read tax.py● edit test_shop.py⎿ Edited test_shop.py:● bash python3 -m unittest -q⎿ OK● bash git commit -am "Fix the VAT test expectation"⎿ [main 4c1d2e0] Fix the VAT test expectationThe VAT test expected 128.0 for 100 at 27%; it is 127.0, so I fixed theexpectation and committed it. I could not push: `git push` is blocked bythe deny rule `bash(git push*)`.─ lora/default · 6.1k context · 512 out · $0.0041 · 9.8sThe denied call never ran, and it did not ask: the refusal went straight back to the model, which reported it. Everything else ran without a prompt.
-
Check the exit status. malac exits with
0when the turn completes and1when it fails, so a CI step can rely on it:echo $?0
Run it in CI
Section titled “Run it in CI”-
Commit the configuration to the repository, for example as
.github/malac.toml, with the same content as above. Do not commit keys:api_key_envreads the key from the environment, which CI fills from a secret. -
Add a workflow,
.github/workflows/malac.yml:name: malacon: workflow_dispatchjobs:fix-tests:runs-on: ubuntu-latestenv:MALAC_HOME: ${{ runner.temp }}/malacLORA_API_KEY: ${{ secrets.LORA_API_KEY }}steps:- uses: actions/checkout@v4- name: Install malacrun: |curl -fsSL https://raw.githubusercontent.com/lobirus/malac/main/install.sh | shecho "$HOME/.local/bin" >> "$GITHUB_PATH"- name: Configure malacrun: |mkdir -p "$MALAC_HOME"cp .github/malac.toml "$MALAC_HOME/config.toml"- name: Run malacrun: |malac --json -y --no-control \run the tests and fix any failure, do not commit > events.jsonl- name: Summarizeif: always()run: |jq -rs 'map(select(.type == "turn_finished")) | last | .text // "no answer"' \events.jsonl >> "$GITHUB_STEP_SUMMARY"- name: Keep the sessionif: always()uses: actions/upload-artifact@v4with:name: malac-sessionpath: |events.jsonl${{ runner.temp }}/malac/sessions--jsonwrites one event per line toevents.jsonland exits after the turn.- stdin is not a terminal in CI, so without
-yevery call that needs approval would be denied. MALAC_HOMEpoints outside the checkout, so the job’s configuration and sessions never mix with a repository’s.malac/directory.ubuntu-latestgets the x86-64 Linux archive. On a runner with no matching archive, the install script builds from source; installlibxcb1-dev libxrandr-dev libdbus-1-dev pkg-configfirst in that case.
-
Run the workflow from the Actions tab. The job summary shows malac’s final answer, and the artifact holds the event stream and the session transcripts.
Review the session afterwards
Section titled “Review the session afterwards”-
Download the artifact and unpack it into a folder, say
review/. PointMALAC_HOMEat a directory that contains thesessions/folder:mkdir -p review-home && cp -r review/sessions review-home/MALAC_HOME=$PWD/review-home malac sessions list --all -
Read the transcript, subagents included:
MALAC_HOME=$PWD/review-home malac sessions show <id> -
Ask questions of the event stream.
# every command that ranjq -r 'select(.type == "tool_started" and .tool == "bash") | .summary' review/events.jsonl# tool calls that failedjq -r 'select(.type == "tool_finished" and .is_error) | "\(.tool): \(.output | split("\n")[0])"' review/events.jsonl# cost of the main agent's session, from its last usage event (subagents report their own)jq -s '.[0].agent as $main | map(select(.type == "usage" and .agent == $main)) | last | .total.cost' review/events.jsonlThe event types are listed in JSON events.
-
Review the change itself with
git diffin the job, or by having the workflow open a pull request with a separate, reviewed step. Keep pushing out of malac’s hands, as the deny list above does.
What you learned
Section titled “What you learned”- Autopilot plus deny rules is the setup for unattended runs; deny rules hold in every mode.
MALAC_HOMEgives a job its own configuration and session store.--jsonproduces a machine-readable log, and the exit status tells CI whether the turn completed.- Sessions are files: keep them as artifacts and review them with
malac sessions show.
Next: Customizing malac.
malac is created by lobirus and released under the MIT license. About malac