SlothGuard - a lazy access reviewer that asks your phone before it revokes anything A developer built SlothGuard, an open-source access-review tool that uses a rules engine to detect segregation-of-duties conflicts and Google's Gemma model only to write short explanations, never to decide findings or severity. In a demo run, 9 rules applied to 40 synthetic users produced 8 findings, with Gemma generating a four-sentence explanation for each in about 10 seconds (97.1 seconds total). Revocations require explicit approval on a phone via a Guardian push, and a denial, a 120-second timeout or a failure all leave permissions unchanged. This is a submission for the Hacktoberfest Open-Source AI Challenge Week 3: Slow and Steady https://dev.to/challenges/hacktoberfest-week3-2026-10-19 Be lazy when you read. Be careful when you approve. Access reviews are the chore everyone postpones. Someone hands you a long list of who can do what, and hidden in it is one question: which pairs of permissions should never belong to the same person? Security people call it segregation of duties SoD . A user who can both create a vendor and release payments to that vendor is the textbook case. SlothGuard does the dull part and keeps the human for the one decision that matters. One rule holds the whole design together: the model never decides findings or severity, and it never touches a permission. It only explains. Everything is synthetic data, and a "revocation" changes a copy kept in memory. Figure 1. Phase 1 detect and explain : the rules engine finds the conflicts and Gemma writes a short explanation for each one. If an answer is missing or breaks the 4-sentence limit, a fixed template is used and the reason is recorded. Figure 2. Phase 2 human approval : nothing is revoked unless the approver answers Approve on the phone. A denial, no answer in 120 s or a failure all mean nothing is revoked, and every step is logged. Full demo, with English captions burned in 7 min 55 s : https://youtu.be/9JlqE3jBITk https://youtu.be/9JlqE3jBITk . The approval on the phone starts at 4:14 https://youtu.be/9JlqE3jBITk?t=254 . The video is also embedded at the end of this post. Phase 1 turned 40 synthetic users and 9 rules into 8 findings, each explained by Gemma. Rule, user and roles come from the data, severity comes from the rule, and the last column is how long the model took in the run of 23 Oct 97.1 s in total, docs/evidence/review-run.md : | Rule | User | Conflicting roles | Severity from the rule | Explanation time | |---|---|---|---|---| | SOD-001 | U001 | Vendor Master Maintain + Payment Release | High | 23.9 s | | SOD-002 | U002 | Purchase Requester + Purchase Approver | High | 10.6 s | | SOD-003 | U003 | Journal Entry Create + Journal Entry Approve | High | 10.4 s | | SOD-006 | U004 | User Admin + Audit Log Admin | High | 11.5 s | | SOD-004 | U010 | Payroll Maintain + Payroll Approve | High | 10.7 s | | SOD-005 | U007 | Bank Reconcile + Payment Release | Medium | 9.8 s | | SOD-007 | U008 | Dev Commit + Prod Deploy | Medium | 10.4 s | | SOD-008 | U009 | Inventory Adjust + Inventory Count Approve | Low | 9.9 s | The first explanation includes loading the model. Figure 3. The findings page. Severity comes from the rules; the Explanation column comes from Gemma. Frame from the demo video. Here is the first finding in full. This is what Gemma wrote in the committed run of 23 Oct, in 4 sentences docs/evidence/report-sample.md . The frame above comes from a later run, so its wording differs: Holding both the Vendor Master Maintain and Payment Release roles creates a significant risk because a single user has the ability to create a vendor and then process payments for that vendor. This conflict violates the Separation of Duties SOD-001 principle, allowing for potential fraud, such as creating fictitious vendors and issuing payments. To mitigate this, review the access controls to ensure that the authority to maintain vendor master data is strictly separated from the authority to release payments. Consider restricting the Payment Release role to only payments against existing, approved vendors. When a person proposes revoking a role, the request reaches the phone as a Guardian push. The page shows Waiting and refreshes by itself. Figure 4. The request on the phone: revoke Vendor Master Maintain from U001. "Negar" is Deny, "Permitir" is Allow. Tenant and e-mail are hidden. The request can end three ways, and each one ends differently: Figure 5. The three outcomes. Approved: the role was revoked from U001 in the program's memory only. Denied: the request for U003 was denied on the phone. Expired: nobody answered the request for U002, so nothing was revoked. Frames from the demo video. Figure 6. The audit trail after the runs: U001 proposed, approved, executed; U002 proposed and expired two attempts ; U003 proposed and denied. Repository: https://github.com/aridiosilva/SlothGuard https://github.com/aridiosilva/SlothGuard A lazy access reviewer. Plain rules find segregation-of-duties SoD conflicts, a small open-weight model running on your own computer explains each one, and nothing is revoked unless a human approves it on their phone through Auth0. Rules detect, the model explains, a human approves. The model never decides what is a finding or how severe it is, and it never touches a permission. Built for the DEV Hacktoberfest Open-Source AI Challenge, Week 3: Slow and Steady. Project started on 2026-10-22 challenge window: 2026-10-19 to 2026-10-25 . All data is synthetic. A "revocation" only changes an in-memory copy of that data. data/synthetic/users.json and SoD rules data/synthetic/sod rules.json . To run it PowerShell, from the project folder : ollama pull gemma4:e2b .\.venv\Scripts\python scripts\run review.py Phase 1: needs Ollama running .\.venv\Scripts\python scripts\run app.py Phase 2: open http://localhost:8000 Use localhost , not 127.0.0.1 : it is the address registered in Auth0. The Auth0 settings go in a local .env file, which git ignores; the README section "Auth0 setup" lists exactly what to configure. Tests: 177 passed and 1 skipped, as the README reports the skipped one needs a running Ollama: set RUN OLLAMA TESTS=1 . The git history shows the test: commits before the matching feat: ones. The central idea, the cap and the fallback around the model, from src/slothguard/llm/ init .py : python def explain finding finding: Finding, rule: SoDRule, client: LLMClient, cache: Optional ExplanationCache = None, max sentences: int = MAX SENTENCES, - Explanation: if cache is not None: hit = cache.get finding if hit: return Explanation text=hit, source="cache" try: answer = client.generate build prompt finding, rule, max sentences except Exception: return Explanation text=fallback text finding, rule , source="fallback", reason="model error" text = clean model text answer if isinstance answer, str else "" if not text: return Explanation text=fallback text finding, rule , source="fallback", reason="empty answer" if count sentences text max sentences: return Explanation text=fallback text finding, rule , source="fallback", reason="too many sentences" if cache is not None: cache.put finding, text return Explanation text=text, source="model" The prompt asks the model to keep to 4 sentences and not to change the severity. The code does not rely on the model listening: severity comes from the rule, and the cap is checked afterwards. First, can this PC run it? Before building the explanation step 22 and 23 Oct , I checked whether this machine could carry a model at all. On paper it looked tight: 15.7 GB of usable RAM in a single channel, and a back-of-the-envelope speed ceiling of about 14 tokens per second from memory bandwidth. Then I measured six requests with Gemma 4 E2B: 13.0 to 21.6 tokens per second, with only 4.2 GB of RAM free before the first one. My ceiling was wrong, so I trusted the measurement over the estimate. Phase 1: rules first, model second. The rules run before the model and decide everything that matters: which user, which rule, which severity. Only then does Gemma 4 E2B the gemma4:e2b tag in Ollama, a 4.6 GB download get the finding and write a short explanation, with thinking turned off. The limit is enforced by the code, not only requested in the prompt: in my first measurements one answer out of six broke it. If one answer fails, comes back empty or goes over the limit, a fixed template is used for that finding and the reason is recorded, so a bad answer never blocks the report. The fallback is not hypothetical: in my first real run of the review script 23 Oct , Ollama was not running, all 8 findings came out as the fixed template in 32.9 s, and the report was still written. Today, if Ollama cannot be reached at all, run review.py stops at once instead of writing a report made only of templates. Phase 2: the human step. The web app lets a reviewer log in through Auth0 Universal Login and propose a revocation from the findings page. The app starts a CIBA request, and Auth0 sends a push to the Guardian app on the phone, carrying a short message such as SoD SOD-001:revoke-Vendor Master Maintain-from-U001 . The page polls GET /proposals/