Skip to main content

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 placeWhyBuilt in
1A baseline: five to eight real tasks, run three times each, with automatic checksYou can't show a control works without a before and afterLab 0
2AGENTS.md with the invariants, protected paths, a never-list and a stop conditionMost runs never hit a control if the agent knows the rulesLab 2
3Project default autonomy set to LowEdits proceed, anything riskier asksLab 4
4Permission rules for pushes, force pushes, database shells and migrations, each with testsAuthority is configuration, reviewable in one fileLab 4
5PreToolUse and Stop hooks on the protected paths, each tested by trying to break itProtected code is protected by mechanismLab 5
6Sandbox enabled, with the network allowlist you actually needWrites outside the workspace and unexpected network calls are stopped by the operating systemThis guide
7Droid Shield left onSecrets in commits Droid makes are caught before they leaveLab 4
8A CI check that fails any pull request with an unapproved protected-path changeThe layer that holds whoever or whatever made the changeLab 9
9A decision on cloudSessionSync and includeCoAuthoredByDroid, recordedBoth default to true. Your data and attribution policy decides, not the defaultThis 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:

  • allow runs without an approval prompt. Hooks and the sandbox still apply.
  • ask always needs approval, even at High autonomy. In headless droid exec nobody can approve, so the run fails.
  • block rejects 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.

.factory/settings.json
{
"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 push and /usr/bin/git push don't match ["git", "push"].
  • Position matters. The force-push rule above expects the flag third. git push origin main --force slips past it, and the ask rule on git push catches 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.match and tests.noMatch, and why droid rules check runs in CI.
  • droid rules check exits 0 even when the decision is block. A script that checks the exit code learns nothing. Read the decision from --json.
  • Rules see commands, not files. An edit through the Edit tool 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 PreToolUse hook on Create|Edit|ApplyPatch that exits 2 when the target is under a protected path. Add a second group on Execute with a commandRegex for commands that mention those paths.
  • A Stop hook that asks git whether anything under a protected path changed, however it happened, and exits 2 to send Droid back to revert it.
.factory/hooks.json
{
"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 2 blocks. Any other non-zero exit doesn't. A hook that exits 1 is 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. Edit works. edit silently 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. The Stop hook 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.

.factory/settings.json (add alongside the rules)
{
"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-command mode, and Droid refuses to start in whole-process mode.
  • 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 denyRead and denyWrite entries can't be overridden by a user's allow always. In droid 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 Stop hook 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 check on 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 exec is read-only unless you pass --auto low|medium|high. ask rules 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.json merges on top of settings.json at 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 hooksDisabled setting exists. Enterprise settings include allowManagedHooksOnly.
  • 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.

RiskControlKindEvidenceKnown gap
Agent changes regulated calculation codePreToolUse hook on file tools and Execute; Stop hook on outcome; CI diff checkEnforced by code.factory/hooks.json, hook scripts, hook tests, CI logsShell guard misses paths built at run time; Stop and CI cover it
Agent pushes unreviewed codeask rule on git push; block on force pushEnforced by config.factory/settings.json, droid rules check --json outputAbsolute paths such as /usr/bin/git don't match a prefix; branch protection covers the remote
Agent connects to a databaseblock rule on database shells; sandbox network allowlistConfig and OSRules file, sandbox settingsRules match command names only; the sandbox covers the network
Secret leaves in a commitDroid Shield; CI secret scanningEnforced by codeSettings, CI logsShield covers only Droid-run git operations
Agent misreads an invariantAGENTS.md; Spec mode review; read-only reviewer droidShapes behavior, checkpointAGENTS.md, saved specs, review outputA request, not a control; the rows above handle what must never happen
Developer disables a local controlOrg-managed settings; CIConfig and CIEnterprise Controls settings, CI logsNeeds 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>' --json in 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.