|
| 1 | +# Codex execution policy — the counterpart to .claude/settings.json. |
| 2 | +# |
| 3 | +# Same principle: block what wrecks the machine, tears down remote state, or |
| 4 | +# destroys work with no recovery path. Ordinary local development is left to |
| 5 | +# prompt rather than blocked — rm -rf node_modules, docker volume rm, |
| 6 | +# compose down -v, artisan tinker and deleting a remote branch stay usable. |
| 7 | +# |
| 8 | +# Verify after editing: |
| 9 | +# codex execpolicy check --pretty --rules .codex/rules/default.rules -- <command> |
| 10 | + |
| 11 | +# ─────────────────────────── git: work and history ─────────────────────────── |
| 12 | + |
| 13 | +prefix_rule( |
| 14 | + pattern = ["git", "push", ["--force", "-f", "--force-with-lease"]], |
| 15 | + decision = "forbidden", |
| 16 | + justification = "Force-push overwrites history other clones already have", |
| 17 | + match = ["git push --force", "git push -f origin dev"], |
| 18 | + not_match = ["git push", "git push origin dev"], |
| 19 | +) |
| 20 | + |
| 21 | +prefix_rule( |
| 22 | + pattern = ["git", "reset", "--hard"], |
| 23 | + decision = "forbidden", |
| 24 | + justification = "Discards uncommitted work with no recovery path", |
| 25 | + match = ["git reset --hard", "git reset --hard HEAD~1"], |
| 26 | + not_match = ["git reset", "git reset --soft HEAD~1"], |
| 27 | +) |
| 28 | + |
| 29 | +prefix_rule( |
| 30 | + pattern = ["git", "clean", ["-f", "-fd", "-fdx", "-ffd"]], |
| 31 | + decision = "forbidden", |
| 32 | + justification = "Deletes untracked files irrecoverably", |
| 33 | + match = ["git clean -f", "git clean -fdx"], |
| 34 | + not_match = ["git clean -n", "git clean --dry-run"], |
| 35 | +) |
| 36 | + |
| 37 | +prefix_rule( |
| 38 | + pattern = ["git", "checkout", "--"], |
| 39 | + decision = "forbidden", |
| 40 | + justification = "Throws away file changes irrecoverably", |
| 41 | + match = ["git checkout -- src/main.ts"], |
| 42 | + not_match = ["git checkout main", "git checkout -b feat/x"], |
| 43 | +) |
| 44 | + |
| 45 | +prefix_rule( |
| 46 | + pattern = ["git", "branch", "-D"], |
| 47 | + decision = "forbidden", |
| 48 | + justification = "Deletes an unmerged branch; -d is the safe form", |
| 49 | + match = ["git branch -D feat/x"], |
| 50 | + not_match = ["git branch -d feat/x", "git branch"], |
| 51 | +) |
| 52 | + |
| 53 | +prefix_rule( |
| 54 | + pattern = ["git", ["filter-branch", "filter-repo"]], |
| 55 | + decision = "forbidden", |
| 56 | + justification = "Rewrites the entire history", |
| 57 | + match = ["git filter-branch --tree-filter true HEAD"], |
| 58 | +) |
| 59 | + |
| 60 | +prefix_rule( |
| 61 | + pattern = ["git", "reflog", "expire"], |
| 62 | + decision = "forbidden", |
| 63 | + justification = "Destroys the rescue path that survives a hard reset", |
| 64 | + match = ["git reflog expire --expire=now --all"], |
| 65 | + not_match = ["git reflog"], |
| 66 | +) |
| 67 | + |
| 68 | +prefix_rule( |
| 69 | + pattern = ["git", "gc", "--prune=now"], |
| 70 | + decision = "forbidden", |
| 71 | + justification = "Drops unreferenced objects, completing what reflog expire starts", |
| 72 | + match = ["git gc --prune=now"], |
| 73 | + not_match = ["git gc"], |
| 74 | +) |
| 75 | + |
| 76 | +prefix_rule( |
| 77 | + pattern = ["git", "stash", ["drop", "clear"]], |
| 78 | + decision = "forbidden", |
| 79 | + justification = "Stashed work is gone without a reflog entry", |
| 80 | + match = ["git stash drop", "git stash clear"], |
| 81 | + not_match = ["git stash", "git stash list", "git stash pop"], |
| 82 | +) |
| 83 | + |
| 84 | +prefix_rule( |
| 85 | + pattern = ["git", "update-ref", "-d"], |
| 86 | + decision = "forbidden", |
| 87 | + justification = "Deletes a ref directly, bypassing the usual safeguards", |
| 88 | + match = ["git update-ref -d refs/heads/dev"], |
| 89 | +) |
| 90 | + |
| 91 | +prefix_rule( |
| 92 | + pattern = ["git", "worktree", "remove", "--force"], |
| 93 | + decision = "forbidden", |
| 94 | + justification = "Removes a worktree including uncommitted changes", |
| 95 | + match = ["git worktree remove --force ../wt"], |
| 96 | + not_match = ["git worktree list", "git worktree remove ../wt"], |
| 97 | +) |
| 98 | + |
| 99 | +# ──────────────────────────── github: remote state ─────────────────────────── |
| 100 | + |
| 101 | +prefix_rule( |
| 102 | + pattern = ["gh", ["repo", "release", "secret"], "delete"], |
| 103 | + decision = "forbidden", |
| 104 | + justification = "Deletes remote resources; a repo takes issues and PRs with it", |
| 105 | + match = ["gh repo delete TitusKirch/scaffold", "gh release delete v1.0.0"], |
| 106 | + not_match = ["gh repo view", "gh release list"], |
| 107 | +) |
| 108 | + |
| 109 | +# ──────────────────────────── os and storage ───────────────────────────────── |
| 110 | + |
| 111 | +prefix_rule( |
| 112 | + pattern = ["rm", ["-rf", "-fr", "-Rf", "-fR"], "/"], |
| 113 | + decision = "forbidden", |
| 114 | + justification = "Recursive delete from the filesystem root", |
| 115 | + match = ["rm -rf /", "rm -rf /etc"], |
| 116 | + not_match = ["rm -rf node_modules", "rm -rf dist"], |
| 117 | +) |
| 118 | + |
| 119 | +prefix_rule( |
| 120 | + pattern = ["sudo", "rm"], |
| 121 | + decision = "forbidden", |
| 122 | + justification = "Elevated deletion outside the workspace", |
| 123 | + match = ["sudo rm -rf /var/lib"], |
| 124 | +) |
| 125 | + |
| 126 | +prefix_rule( |
| 127 | + pattern = [["chmod", "chown"], "-R"], |
| 128 | + decision = "forbidden", |
| 129 | + justification = "Recursive ownership or permission changes can break the system", |
| 130 | + match = ["chmod -R 777 /", "chown -R root:root /usr"], |
| 131 | + not_match = ["chmod +x script.sh"], |
| 132 | +) |
| 133 | + |
| 134 | +prefix_rule( |
| 135 | + pattern = [["dd", "mkfs", "shred", "fdisk", "parted"]], |
| 136 | + decision = "forbidden", |
| 137 | + justification = "Writes to block devices; never needed from a repo", |
| 138 | + match = ["dd if=/dev/zero of=/dev/sda", "mkfs.ext4 /dev/sdb1"], |
| 139 | +) |
| 140 | + |
| 141 | +# ──────────────────────────── databases ────────────────────────────────────── |
| 142 | + |
| 143 | +prefix_rule( |
| 144 | + pattern = ["php", "artisan", ["migrate:fresh", "migrate:reset", "db:wipe"]], |
| 145 | + decision = "forbidden", |
| 146 | + justification = "Rebuilds or empties the database", |
| 147 | + match = ["php artisan migrate:fresh --seed", "php artisan db:wipe"], |
| 148 | + not_match = ["php artisan migrate", "php artisan tinker"], |
| 149 | +) |
| 150 | + |
| 151 | +prefix_rule( |
| 152 | + pattern = [["prisma", "npx", "pnpm", "bunx"], "prisma", "migrate", "reset"], |
| 153 | + decision = "forbidden", |
| 154 | + justification = "Drops and recreates the database", |
| 155 | + match = ["npx prisma migrate reset", "bunx prisma migrate reset"], |
| 156 | + not_match = ["npx prisma migrate dev", "npx prisma generate"], |
| 157 | +) |
| 158 | + |
| 159 | +prefix_rule( |
| 160 | + pattern = [["pnpm", "npm", "yarn", "bun"], ["db:fresh", "db:reset"]], |
| 161 | + decision = "forbidden", |
| 162 | + justification = "Project wrapper around a destructive database task", |
| 163 | + match = ["pnpm db:fresh", "npm db:reset"], |
| 164 | + not_match = ["pnpm db:migrate", "pnpm db:seed"], |
| 165 | +) |
| 166 | + |
| 167 | +prefix_rule( |
| 168 | + pattern = ["redis-cli", ["flushall", "flushdb", "FLUSHALL", "FLUSHDB"]], |
| 169 | + decision = "forbidden", |
| 170 | + justification = "Empties the whole Redis instance", |
| 171 | + match = ["redis-cli flushall"], |
| 172 | + not_match = ["redis-cli ping"], |
| 173 | +) |
| 174 | + |
| 175 | +# ──────────────────── terraform / opentofu: remote state ───────────────────── |
| 176 | + |
| 177 | +prefix_rule( |
| 178 | + pattern = [["terraform", "tofu"], ["destroy", "force-unlock"]], |
| 179 | + decision = "forbidden", |
| 180 | + justification = "Tears down infrastructure or breaks the state lock", |
| 181 | + match = ["terraform destroy", "tofu force-unlock 1234"], |
| 182 | + not_match = ["terraform plan", "tofu init"], |
| 183 | +) |
| 184 | + |
| 185 | +prefix_rule( |
| 186 | + pattern = [["terraform", "tofu"], "apply", "-auto-approve"], |
| 187 | + decision = "forbidden", |
| 188 | + justification = "Applies infrastructure changes without a confirmation step", |
| 189 | + match = ["terraform apply -auto-approve"], |
| 190 | + not_match = ["terraform apply", "terraform plan"], |
| 191 | +) |
| 192 | + |
| 193 | +prefix_rule( |
| 194 | + pattern = [["terraform", "tofu"], "state", ["rm", "push"]], |
| 195 | + decision = "forbidden", |
| 196 | + justification = "Corrupts or orphans remote state", |
| 197 | + match = ["terraform state rm aws_instance.web", "tofu state push new.tfstate"], |
| 198 | + not_match = ["terraform state list", "terraform state show aws_instance.web"], |
| 199 | +) |
| 200 | + |
| 201 | +prefix_rule( |
| 202 | + pattern = [["terraform", "tofu"], "workspace", "delete"], |
| 203 | + decision = "forbidden", |
| 204 | + justification = "Removes a workspace and its state", |
| 205 | + match = ["terraform workspace delete staging"], |
| 206 | + not_match = ["terraform workspace list", "terraform workspace select prod"], |
| 207 | +) |
| 208 | + |
| 209 | +# ──────────────────────────── aws: remote resources ────────────────────────── |
| 210 | + |
| 211 | +prefix_rule( |
| 212 | + pattern = ["aws", "s3", ["rb", "rm"]], |
| 213 | + decision = "forbidden", |
| 214 | + justification = "Deletes buckets or objects", |
| 215 | + match = ["aws s3 rb s3://bucket --force", "aws s3 rm s3://bucket --recursive"], |
| 216 | + not_match = ["aws s3 ls", "aws s3 cp file s3://bucket/"], |
| 217 | +) |
| 218 | + |
| 219 | +prefix_rule( |
| 220 | + pattern = ["aws", "ec2", ["terminate-instances", "delete-volume", "delete-snapshot"]], |
| 221 | + decision = "forbidden", |
| 222 | + justification = "Destroys compute or storage", |
| 223 | + match = ["aws ec2 terminate-instances --instance-ids i-123"], |
| 224 | + not_match = ["aws ec2 describe-instances"], |
| 225 | +) |
| 226 | + |
| 227 | +prefix_rule( |
| 228 | + pattern = ["aws", "rds", ["delete-db-instance", "delete-db-cluster"]], |
| 229 | + decision = "forbidden", |
| 230 | + justification = "Deletes a database instance", |
| 231 | + match = ["aws rds delete-db-instance --db-instance-identifier prod"], |
| 232 | + not_match = ["aws rds describe-db-instances"], |
| 233 | +) |
| 234 | + |
| 235 | +prefix_rule( |
| 236 | + pattern = ["aws", "cloudformation", "delete-stack"], |
| 237 | + decision = "forbidden", |
| 238 | + justification = "Tears down every resource in the stack", |
| 239 | + match = ["aws cloudformation delete-stack --stack-name prod"], |
| 240 | + not_match = ["aws cloudformation describe-stacks"], |
| 241 | +) |
| 242 | + |
| 243 | +prefix_rule( |
| 244 | + pattern = ["aws", "iam", ["delete-user", "delete-role", "delete-policy", "delete-group"]], |
| 245 | + decision = "forbidden", |
| 246 | + justification = "Removes identities other systems depend on", |
| 247 | + match = ["aws iam delete-role --role-name deploy"], |
| 248 | + not_match = ["aws iam list-roles", "aws iam get-role --role-name deploy"], |
| 249 | +) |
0 commit comments