Guardrails for regulated repos
This guide is for the engineer preparing a proof of concept with Droid somewhere that code changes get audited: a bank, an insurer, a health system, anything with a model-risk function or a change-advisory board. It covers what to put in place before the first delegation, and how to walk a skeptical reviewer through it.
The kit has five parts, from the inside out: permission rules, hooks, the sandbox, Droid Shield and CI. None of them is enough alone. Each one covers part of what the others can't see.
This map shows what each layer checks (orange is enforced, dark is a person deciding), not the order of the checks on one tool call, which the docs don't specify.
To keep the map readable it draws only the sandbox's arrow from Execute. The sandbox also checks the file tools, web tools, MCP tools, subagents and hooks.
Before the first delegation
Put these in place before anyone asks Droid to change code in the target repository. Each links to where the workshop builds it.
| # | In place | Why | Built in |
|---|---|---|---|
| 1 | A baseline: five to eight real tasks, run three times each, with automatic checks | You can't show a control works without a before and after | Lab 0 |
| 2 | AGENTS.md with the invariants, protected paths, a never-list and a stop condition | Most runs never hit a control if the agent knows the rules | Lab 2 |
| 3 | Project default autonomy set to Low | Edits proceed, anything riskier asks | Lab 4 |
| 4 | Permission rules for pushes, force pushes, database shells and migrations, each with tests | Authority is configuration, reviewable in one file | Lab 4 |
| 5 | PreToolUse and Stop hooks on the protected paths, each tested by trying to break it | Protected code is protected by mechanism | Lab 5 |
| 6 | Sandbox enabled, with the network allowlist you actually need | Writes outside the workspace and unexpected network calls are stopped by the operating system | This guide |
| 7 | Droid Shield left on | Secrets in commits Droid makes are caught before they leave | Lab 4 |
| 8 | A CI check that fails any pull request with an unapproved protected-path change | The layer that holds whoever or whatever made the change | Lab 9 |
| 9 | A decision on cloudSessionSync and includeCoAuthoredByDroid, recorded | Both default to true. Your data and attribution policy decides, not the default | This guide |
Items 1 to 5 and 8 live in the repository. That matters for the review: a reviewer can read them, run them and diff them over time.
1. Permission rules
Permission rules decide what happens to a shell command before the model has a say. They live under permissionRules in .factory/settings.json, in JSON. Each rule has one of three decisions:
allowruns without an approval prompt. Hooks and the sandbox still apply.askalways needs approval, even at High autonomy. In headlessdroid execnobody can approve, so the run fails.blockrejects the command with no approval path, even when prompts are skipped.
There is no deny. A rule with "decision": "deny" is invalid, and invalid rules are skipped at run time; only droid rules check tells you.
Check one thing before you write any: the permission-rules docs say to confirm that permission rules are enabled for your organization and supported by your Droid version, because checking a rules file validates it but doesn't turn enforcement on. If the Permission Rules section is missing from Enterprise Controls, the docs send you to Factory support.
{
"sessionDefaultSettings": {
"autonomyLevel": "low"
},
"permissionRules": {
"version": 1,
"rules": [
{
"id": "project/git-push",
"decision": "ask",
"reason": "A human pushes. Every time.",
"match": { "prefix": ["git", "push"] },
"tests": {
"match": ["git push origin main", "git push"],
"noMatch": ["git status", "git pull"]
}
},
{
"id": "project/no-force-push",
"decision": "block",
"reason": "History on shared branches is evidence.",
"match": { "prefix": ["git", "push", ["--force", "-f", "--force-with-lease"]] },
"tests": {
"match": ["git push --force origin main", "git push -f"],
"noMatch": ["git push origin main"]
}
},
{
"id": "project/no-database-shells",
"decision": "block",
"reason": "No agent session connects to a database directly.",
"match": { "prefix": [["psql", "mysql", "mongosh"]] },
"tests": {
"match": ["psql -h ledger-prod.internal", "mongosh"],
"noMatch": ["npm run db:migrate:check"]
}
}
]
}
}
Check them after every edit:
droid rules check droid rules check --command 'git push origin main --force' --json
What a reviewer should know about rules:
- Matching is literal. Prefixes are exact, case-sensitive tokens in order.
*is a literal star, not a wildcard.git -C /repo pushand/usr/bin/git pushdon't match["git", "push"]. - Position matters. The force-push rule above expects the flag third.
git push origin main --forceslips past it, and theaskrule ongit pushcatches it. Layer rules; don't rely on one. - A broken rule fails open. A rule that fails its own tests is skipped, not turned into block-all. Worse, duplicate IDs or an invalid container can make Droid skip that whole settings source. That's why every rule carries
tests.matchandtests.noMatch, and whydroid rules checkruns in CI. droid rules checkexits0even when the decision isblock. A script that checks the exit code learns nothing. Read the decision from--json.- Rules see commands, not files. An edit through the
Edittool isn't a shell command. Protecting paths is the hooks' job.
2. Hooks
A hook is your own script, run by Droid at a fixed point in the session. It's the layer that turns "please don't edit the ledger code" into "the ledger code cannot be edited". Hooks live in .factory/hooks.json, with the event names at the top level.
The minimum for a protected path is two hooks:
- A
PreToolUsehook onCreate|Edit|ApplyPatchthat exits2when the target is under a protected path. Add a second group onExecutewith acommandRegexfor commands that mention those paths. - A
Stophook that asks git whether anything under a protected path changed, however it happened, and exits2to send Droid back to revert it.
{
"PreToolUse": [
{
"matcher": "Create|Edit|ApplyPatch",
"hooks": [
{ "type": "command", "command": "node \"$FACTORY_PROJECT_DIR\"/.factory/hooks/protect-paths.mjs" }
]
},
{
"matcher": "Execute",
"commandRegex": "(src/ledger|migrations/|vendor/)",
"hooks": [
{ "type": "command", "command": "node \"$FACTORY_PROJECT_DIR\"/.factory/hooks/protect-shell.mjs" }
]
}
],
"Stop": [
{
"hooks": [
{ "type": "command", "command": "node \"$FACTORY_PROJECT_DIR\"/.factory/hooks/protected-diff.mjs" }
]
}
]
}
What a reviewer should know about hooks:
- Exit
2blocks. Any other non-zero exit doesn't. A hook that exits1is a non-blocking error, and the tool runs anyway. Test every hook by piping sample JSON into it and checking the exit code. - Matchers are case-sensitive.
Editworks.editsilently matches nothing. - The shell guard is a pattern match. It stops
sed -i src/ledger/.... It misses a script that builds the path at run time. Say so. TheStophook and CI exist because of it. - Hooks load at session start. Droid snapshots them and warns when they change underneath a running session.
- Test by attacking. Give Droid a task that pushes toward the protected change and tells it to find another way if blocked. Watch what it reaches for. Lab 5 walks through this, and the hook stepper shows it without installing anything.
3. The sandbox
Rules and hooks guard what you name. The sandbox restricts what every command can do at the operating-system level: Seatbelt on macOS, bubblewrap plus seccomp on Linux and WSL2, and a filtering proxy for network. It covers file tools, Execute, web tools, MCP tools, subagents and hooks.
{
"sandbox": {
"enabled": true,
"mode": "per-command",
"filesystem": {
"denyRead": ["~/.aws/credentials", "~/.ssh/id_rsa"]
},
"network": {
"allowedDomains": ["github.com", "*.npmjs.org"]
}
}
}
By default, writes are denied everywhere except the working directory, and network is denied except Factory's own and its authentication provider's domains. Add only the domains the task needs.
What a reviewer should know about the sandbox:
- It fails closed. When isolation is unavailable, the command is blocked in
per-commandmode, and Droid refuses to start inwhole-processmode. - Violations interrupt the session with a prompt, even at High autonomy. The options are allow once, allow always (saved to user settings) or deny. A network prompt for a shell command auto-denies after 60 seconds. Org-level
denyReadanddenyWriteentries can't be overridden by a user's allow always. Indroid exec, violations are auto-denied without a prompt. - Sandboxed processes can't write to Droid's own configuration files. A command can't loosen its own restrictions.
- It costs prompts. Expect interruptions in the first week while you tune the allowlist. That's the price, and it's worth paying in a regulated POC.
- Settings resolve org, then project, then user. Put the sandbox in managed org settings if it must hold for every engineer.
4. Droid Shield
Droid Shield scans for secrets when Droid runs git commit (staged changes) or git push (commits about to be pushed). It looks only at added lines, uses pattern matching, a randomness check and context, and blocks the git operation if it finds something that looks like a key, token, private key or credentialed URL. It's on by default (enableDroidShield: true), and organization admins can enforce it through Enterprise Controls.
What a reviewer should know:
- It only covers git operations Droid performs. A commit you make yourself in another terminal isn't scanned. Keep your existing secret scanning in CI.
- There is no documented allowlist. The documented overrides are committing manually or turning Shield off in
/settings. - Leave it on. It costs nothing on a clean commit.
5. CI: the layer that doesn't trust the session
Everything above runs on the engineer's machine, inside Droid. CI runs somewhere else, on every change, whoever made it. For a regulated repository it's the backstop.
- A protected-path check. Diff the pull request against its base branch. Fail it if anything under a protected path changed without an approval your process recognizes, such as a label, a CODEOWNERS review or a linked change ticket. A log your
Stophook writes on the developer's machine is useful local evidence, but CI only sees it if you commit it. The pull request diff is what CI can trust. droid rules checkon every pull request that touches.factory/. A rule that fails its tests is skipped silently. CI is where you find out.- Headless runs stay bounded.
droid execis read-only unless you pass--auto low|medium|high.askrules make a headless run fail, which is how "a human pushes" survives automation. - Droid code review through
Factory-AI/droid-action, with your own guidelines in.factory/skills/review-guidelines/SKILL.md. It's a second reader, not an approver of record.
The Droid in CI guide has the workflow patterns.
What a developer can change locally
A reviewer will ask this, and the honest answer has several parts. Know them before the meeting.
- Local overrides.
settings.local.jsonmerges on top ofsettings.jsonat the same level. The permission-rules page says that within a folder's settings the local file claims rule IDs first, and that a disabled definition still claims its ID. Read together, a developer's local file could disable a shared rule by reusing its ID. Try it in your version before the meeting so you can answer from what you saw. - Hooks. A
hooksDisabledsetting exists. Enterprise settings includeallowManagedHooksOnly. - Droid Shield can be toggled in
/settings, unless your organization enforces it. - Workspace trust. Project and folder rules require it. An untrusted folder doesn't get them.
- Runtime settings.
--settings <path>applies a settings file for one process. For permission rules, that Runtime level claims IDs ahead of the project's rules. Only Org outranks it.
The mitigation is the same in every case. Controls that must hold for everyone go in organization settings through Enterprise Controls. There, org rules claim IDs before anything else, hard controls such as sandbox, managed hooks and maxAutonomyLevel can't be weakened by lower levels, and allowManagedHooksOnly makes Droid ignore project and user hooks entirely. And CI, which the developer doesn't control, checks the outcome. The repository-level harness is for the developer who wants to do the right thing. Org settings and CI are for the day someone doesn't.
Presenting it to a model-risk or security reviewer
Your reviewer doesn't want to know what the agent usually does. They want to know what it cannot do, how you know, and where they can see that for themselves. Structure the conversation around those three questions.
Bring a control matrix
One row per risk. Say which layer handles it, whether that layer asks or enforces, where the evidence is, and what it misses.
| Risk | Control | Kind | Evidence | Known gap |
|---|---|---|---|---|
| Agent changes regulated calculation code | PreToolUse hook on file tools and Execute; Stop hook on outcome; CI diff check | Enforced by code | .factory/hooks.json, hook scripts, hook tests, CI logs | Shell guard misses paths built at run time; Stop and CI cover it |
| Agent pushes unreviewed code | ask rule on git push; block on force push | Enforced by config | .factory/settings.json, droid rules check --json output | Absolute paths such as /usr/bin/git don't match a prefix; branch protection covers the remote |
| Agent connects to a database | block rule on database shells; sandbox network allowlist | Config and OS | Rules file, sandbox settings | Rules match command names only; the sandbox covers the network |
| Secret leaves in a commit | Droid Shield; CI secret scanning | Enforced by code | Settings, CI logs | Shield covers only Droid-run git operations |
| Agent misreads an invariant | AGENTS.md; Spec mode review; read-only reviewer droid | Shapes behavior, checkpoint | AGENTS.md, saved specs, review output | A request, not a control; the rows above handle what must never happen |
| Developer disables a local control | Org-managed settings; CI | Config and CI | Enterprise Controls settings, CI logs | Needs org settings in place |
Be explicit about the "Kind" column. The most useful sentence in the meeting is "this row is a request, and here is the control behind it".
Show, don't describe
- Run
droid rules check --command '<a dangerous command>' --jsonin front of them. - Pipe a sample event into a hook and show exit
2. - Show a session where the hook blocked the edit and Droid tried another route. Then show the second hook blocking that route.
- Show the scoreboard before and after, with the protected-path attempt count as well as the pass rate. A rising pass rate with falling attempts is a harness working. A rising pass rate with steady attempts is luck.
Say what's illustrative
If any number in your deck came from a demo rather than a measured run, label it. A reviewer who catches one overclaim stops trusting the rest.
Agree what "signed" means
Leave with a written list: which controls are in scope, which are org-level and need an owner outside your team, what evidence the reviewer wants on a schedule, and what would make them withdraw approval. That list is the real deliverable of the POC. The code is how you earned it.