Repository navigation
AI Policy
This page describes the policy slice of the core plugin claudetto.
It is for a user who wants to know why Claude stops before it contributes to a repository of another owner.
The slice reads the AI policy of such a repository first, and it follows that policy.
It never stops local work.
You can read and change the code in your own fork or in a private clone.
It stops only an action that reaches the upstream repository.
The gate runs on an action that sends work to a repository whose owner is not your GitHub login.
Your login comes from gh config get user -h github.com, which makes no network call.
A repository of an organization is external, unless you are an owner of the organization.
The slice finds your organizations with gh api user/memberships/orgs, and an organization where your role is admin counts as your own.
When that lookup fails, each organization is external.
When the login is unknown, each repository is external.
A push to your own fork passes.
A pull request from your fork to its parent is an action on the parent.
These stay allowed in each checkout, also in a checkout with a forbidden verdict:
-
Edit,Write, andNotebookEdit. - Local
Bashcommands, such asgit commit,git fetch, builds, and redirects. -
ghgroups that work on local state:auth,config,alias,completion,extension,pr checkout,repo clone, andrepo fork. -
curlandwgetcalls that only read.
The exception is the verdict store.
The slice denies each call that is not a read and names .claudetto/policy.
-
git push. The slice finds the target from the remote or URL in the command. Without one, it uses the push remote of the branch, ororigin, withgit remote get-url --push. A remote with a path, and a remote thatgitcannot find, pass. -
ghwrites, such asgh pr create,gh issue create,gh release, andgh apiwith a method other thanGET. The target comes from-R, then from the checkout, and a fork gives its parent. - GitHub MCP tools whose name has a write verb.
-
curl,wget,http, andxhcalls that write togithub.com,api.github.com, oruploads.github.comand name an external repository.
A folder, a remote, or an address that a shell expands cannot be found.
The slice denies such an action only when a verdict forbidden exists.
The first gated action for an external repository is denied.
The deny lists the policy files that the slice found, and it asks Claude to read each one.
The slice looks in the checkout, or in the repository on GitHub when there is no checkout.
It also looks in the .github repository of the owner and in the parent repository of a fork.
The slice finds a file by its name and its place only.
It never reads a policy text for meaning.
The places are the root folder, .github, .claude, docs, doc, policies, docs/guidelines, doc/internals, and doc/contribute.
These names are in the list:
-
AGENTS.md,CLAUDE.md,GEMINI.md,.cursorrules, andcopilot-instructions.md. -
CONTRIBUTING*,CONTRIBUTION*,CONVENTIONS*,HACKING*,SUBMITTINGPATCHES*,AI_POLICY*,AI-POLICY*,CODE_OF_CONDUCT*, andREADME*. - Each file with
AIorLLMas a word in its name, such asAI_USAGE_POLICY.md,AI.md, andai-contribution-policy.md. - The files in
.github/PULL_REQUEST_TEMPLATE,.github/ISSUE_TEMPLATE,.gitlab/merge_request_templates,.gitlab/issue_templates,.cursor/rules, andCONTRIBUTING.d.
The catalog open-source-ai-contribution-policies shows these names.
About half of its projects publish the policy on a website or a wiki with no file in the repository.
The slice cannot find such a policy.
Claude reads the files and calls the tool ai_policy with one verdict:
-
allowed: a file allows AI tools with no rule. -
silent: no file says anything about AI tools. -
conditions: a file allows AI tools under rules. -
forbidden: a file forbids AI tools, LLM contributions, or generated code.
The tool refuses a verdict when read leaves out a listed file.
It also refuses a verdict other than silent when the quote is not word for word in a listed file.
When no file exists and none is unread, the slice records silent and the action passes.
With the verdict forbidden, each gated action for that repository is denied.
The deny quotes the policy and tells Claude to tell you and to ask you what to do.
Your edits, commits, and builds in your own copy stay allowed.
A verdict silent means that Claude found no AI policy in the repository.
The project can keep its policy on a website or on a mailing list, where Claude cannot read it.
So a repository of another owner gets no agent-made push, gh write, MCP write, or network write without your consent.
For each such action, the tool.check handler of the slice returns an ask.
The text names the repository and asks you to approve only if you know that the project accepts AI contributions.
The consent is for one action and is not kept.
The next action asks again.
The verdicts allowed and conditions do not ask, and forbidden still denies.
No permission mode approves this ask by itself.
bypassPermissions, auto, and acceptEdits show the prompt.
dontAsk and -p runs deny the call, which is safe.
A mod gets no permission mode, so one design holds in each mode.
If Claude Code offers to remember the choice, the slice asks again, because the stricter verdict wins.
The auto mode is an inference: the docs state it for hooks, and the mod path uses the same pipeline.
When the remote of a push or a gh write cannot be found, or the command cannot be read, and the session folder is in an external repository, tool.check asks you.
In other folders the call passes to the push hook.
When the check itself fails, the slice denies a gated action at tool.call and passes every other call.
A failed check on Edit passes.
A failed check on git push, on a gh write, on a network write, or on a GitHub MCP write is denied.
At tool.check, a failed check on such an action asks you, and never passes.
/claudetto:setup writes the env keys of a git pre-push hook, next to the commit-msg hook.
Git runs it for each push that Claude makes, also from a script, an alias, or a subshell, and gives it the real remote.
The slice sets CLAUDETTO_AGENT_CALL while a Bash call of Claude runs, and the hook checks a push only when that name is set.
So a ! command that you type yourself, such as ! git push, passes the hook.
The hook is a shell script that reads plain files in ~/.claudetto/policy/, with no jq and no gh.
It works by these rules:
- A push to a repository of the user passes.
This includes each organization in
~/.claudetto/policy/orgs, which the slice writes at session start. - A push with the verdict
forbiddenor with no verdict is rejected, and the note tells Claude to run a plaingit push. - A push with the verdict
silentpasses once, with the consent token that the slice armed at theask. The slice clears the token at the next tool call, and the hook ends it after 15 minutes. - A push with the verdict
allowedorconditionspasses.
The slice denies a Bash push that skips or changes the hook:
--no-verify, -c hook.*, core.hooksPath, a change of GIT_CONFIG_*, env -i, and env -u of such a name.
It also denies each Bash command that sets or removes CLAUDETTO_AGENT_CALL.
A script, a build, or an interpreter can call the GitHub API, and the slice cannot read what it does.
When a Bash call runs such a program in a checkout that is not yours and has no verdict allowed or conditions, the slice sets GH_TOKEN and GITHUB_TOKEN to a value that is not a token.
It restores the two values after the call.
The checkout is the session folder or a folder of a cd in the command.
The blank is a soft barrier.
A script can still read a token from the keychain, from the config of gh, or from another file, and it can push over SSH.
GH_ENTERPRISE_TOKEN is not blanked.
A gh command in the same command as the script also gets no token.
The hard barrier is /claudetto:setup sandbox, because a plugin cannot change the sandbox settings from a hook.
It acts only in a checkout that is not yours and has no verdict allowed or conditions.
After your approval, it writes these keys to .claude/settings.local.json of the checkout:
-
sandbox.enabledistrue. -
sandbox.allowUnsandboxedCommandsisfalse, so Claude Code ignoresdangerouslyDisableSandbox. -
sandbox.failIfUnavailableistrue, so Claude Code does not start when the sandbox cannot start. Without it, Claude Code runs the commands with no sandbox in that case. -
sandbox.network.deniedDomainsgetsgithub.comand*.github.com. A denied host stays blocked when anallowedDomainsentry matches it too.
The hard stop has these costs in that checkout:
- Each shell command of Claude runs in the sandbox.
- Each connection to GitHub from a shell command fails, also a read such as
git fetch,gh pr view, or a script that reads the GitHub API. - A connection to a host that is not in
sandbox.network.allowedDomainsasks for your approval, or fails in auto mode. - Claude Code does not start in the checkout when the sandbox cannot start.
On Linux and WSL2, the sandbox needs
bubblewrap,socat, and the seccomp filter helper of@anthropic-ai/sandbox-runtime. Without the filter, a command can reach the socket ofssh-agent, and on WSL2 it can startgit.exewith Windows credentials. So setup checks them first and writes nothing when one is missing. It also writes nothing whensandbox.network.allowAllUnixSocketsistrue, or when a test run of thebwrapthat Claude Code uses fails. It gives the install command, and/claudetto:setup sandboxruns it only after your approval. Native Windows and WSL1 have no sandbox, so setup writes nothing there.
The hard stop does not cover a command that you type at the ! prompt, an MCP tool, or a hook, because they run outside the sandbox.
The gate of this slice still checks the MCP tools.
An entry of sandbox.excludedCommands also runs outside the sandbox, so setup names each entry that it finds.
*.githubusercontent.com stays open, because it serves files to read and no endpoint that writes.
Setup keeps a record of its changes in .claudetto/sandbox-stop.json.
/claudetto:setup sandbox with --remove takes out only those changes and puts back each old value.
After Claude records a verdict silent or forbidden for a checkout with no hard stop, the reply of the tool tells Claude to offer the command.
Each verdict is in ~/.claudetto/policy/, with a hash of the policy files.
A stored verdict is reused only when the hash is the same.
When a file changes, Claude reads the files again.
The slice denies each call that is not a read and names .claudetto/policy, also with a glob.
This covers Edit, Write, NotebookEdit, a redirect, and a command such as cp.
This holds also when the policy gate finds no external repository.
Files from GitHub are cached in ~/.claudetto/policy/cache/ so that Claude can read them with Read.
The slice reads argv, tool names, and URLs. It cannot read what a script does, so these stay open:
- Code that an interpreter or a script runs, such as
python script.pyorpython -cthat callsgit push. The push hook checks the push, and the token blank makes aghwrite or an HTTP write from a script harder, but does not stop it. Claude Code has no documented point to put a shim inPATHfrom a mod, andsandbox.networkfilters by host and not by method. - A shell trick that hides a folder or program.
A dynamic folder or remote is denied only when a verdict
forbiddenexists. - Symlinks and aliases for
ghand HTTP. A push is checked by the hook. - Network clients other than the ones in the list, and
curlinside a script. - A
cdinside a subshell, forghand HTTP. The hook checks a push there. - A script that clears its environment (
env={}) hides the hook from git. - A consent token is armed at the
ask, before you answer, and the next tool call clears it. A mod cannot see your answer and cannot delete a file, so it writes an empty file. - Git connects to the remote before it runs
pre-push, so the hook sees only a push that can connect. - A
!command that you type while aBashcall of Claude runs gets the marker, and it can get the blank tokens. The hook then checks your push, which fails closed. - In a session where the organization of your account manages the settings,
$.env.setcan refuse a change. Then a push from a script has no marker and passes the hook, and only thetool.callgate checks a plaingit push. - The file of your organizations changes only at session start.
- Git older than 2.54 runs no config hook.
Then a plain
git pushis still gated by thetool.calldeny and thetool.checkask, but a push from a script is not checked./claudetto:setupsays so when it finds an older git. - The hash is for change detection only.
- A
404in thegherror text means that a file is absent.
The option aiPolicy is a boolean and is on by default.
Set it to false to turn the gate off.
With the option off, the plugin registers no tool.
- Guards: the other slices that deny a call.
- Architecture: the slices and the kernel.
- Start
- How claudetto Works
- Add-On Plugins
- How Claude Code Works
- Reference
- Rationale
- Evidence
- Development
- Archive