Skip to content
ThreefoldAgent governance

Connect

Connect a coding agent

One hook supports Claude Code, Codex, Antigravity and Muse, checking supported writes and commands before they run. Architecture rules start in Observe so you can review what they would refuse before promoting them. Credential and hook-protection checks remain active locally.

Endpoint Not checked

The snippets below rewrite themselves from this field, so what you copy is what this page can reach.

Connect a repository in one command

Run it at the root of the repository. It installs the hook for Claude Code, Codex, Antigravity and Muse where they are installed, keeps its files out of git, starts the project in Observe, sends one harmless call to prove the path, and opens the project on the dashboard.

Guided, with a watch for the first call

          
        
  1. 1

    Stage 1 · Observe architecture rules

    Every project starts here. Architecture rules record what they would refuse. Known credentials and attempts to disable the hook are still refused locally, and a call the hook holds back is not sent. The dashboard lists what each rule would have refused, and you mark each one correct or false alarm.

  2. 2

    Stage 2 · Enforce, when you are confident

    A rule whose flags were all correct reads Ready. Promote the project from its page on the dashboard with the rules you choose: those refuse from the next call, and the others keep observing. Back to Observe is one click.

The command runs the file this stack serves as install.py, as readable text, so it can be read before it is run. On a private stack, python threefold.py open signs this browser in to the dashboard. The installer and the manual steps below remain for a checkout of the repository.

What the hook changes

The scenarios on the demo build their own payloads and then refuse them. That proves the gates run; it intercepts nobody. This hook is the difference: the agent runs it before every matched tool call and hands it the call it is about to make. The hook asks this service and, on a refusal, answers in the format that agent reads as a deny. On an approval it prints nothing, so the agent's own permission prompts run exactly as before.

  1. 1

    The agent proposes a tool call

    A write, an edit or a shell command, picked out by the matcher in that agent's hook file.

  2. 2

    The hook asks this service

    One POST /evaluate-tool-call, standard library only, no dependencies to install. Some calls are never sent; the list is below.

  3. 3

    A refusal is handed back as a deny

    Measured on 2026-09-21: Claude Code and the Antigravity desktop app stopped the refused write. Measured on 2026-09-23, in one run: Codex stopped it over apply_patch. Measured on 2026-09-28: Muse stopped it over write_file, edit_file and the shell. The table further down has the detail, and says which Codex routes are still not measured.

Govern your own repositories

The file the command above downloads, run once per repository, for all four agents and the commit. A new project starts in Observe; promoting it on the dashboard is what enforces.

Observe, then promote

The installer · threefold.py

The file this stack serves as install.py, saved as threefold.py by the command above: standard library only, run at the root of each repository you want governed. --api-key-file is needed only where the stack enforces a key, and it names a file holding the key, so the key itself never appears in a command line or in the repository.


              
            

What it writes

  • .threefold.json at the repository root, holding project, endpoint, mode and, when given, api_key_file: the path to the key, never the key.
  • The three agents' hook settings: .claude/settings.local.json for Claude Code, .codex/hooks.json for Codex and .agents/hooks.json for Antigravity, the same blocks as Step 2 below.
  • The Muse plugin bundle, .threefold-muse/, holding the hook and its manifest, registered for the project with muse plugins install and approved with muse plugins approve, the same commands as Step 2 below.
  • The pre-commit hook, so an edit that got past an agent is still judged when it is committed, and recorded with agent pre-commit.
  • Every one of them listed in .git/info/exclude, which is local to your clone. None of it is ever committed, and the governed repository shows no change.

            

The hook reads each setting from the environment first, then from .threefold.json, then from ~/.threefold/config.json, so a variable set where the agent starts wins over the file.

From Observe to Enforce

The stage is the project's, on the service, not your machine's. connect installs in managed mode, so the project's stage decides every call, and a new project starts in Observe: architecture rules record what they would refuse. Credential and hook-protection checks remain active locally in every stage: a call carrying a credential is still refused on your machine and never sent; attempts to disable the hook are refused locally too. A call the hook holds back (outside the project, a data file, a term on your never-send list) is neither sent nor recorded.

When that record reads right, enforcement is one step: promote the project on the dashboard, choosing the rules that enforce. Nothing needs to be installed again.

Two other modes change only this machine. --mode observe caps it to Observe: every call goes as a dry run, never refused, whatever the stage. --mode enforce sends calls exactly the same way as managed; what it adds is on the machine, where a file-tool write to the hooks' own files is refused whatever stage was last seen.

Until it is promoted the project stays in Observe whatever mode your machine is in, so a forbidden write is still recorded and the agent is told nothing. A stack deployed with DefaultHookStage=enforce puts projects that have no stage of their own straight into Enforce; the public demo deploys with the default, observe.


          

Disconnect

Run threefold.py disconnect at the root of the same repository; threefold.py status lists every folder this machine governs. By hand it is the same list in reverse: delete .threefold.json, take the Threefold block out of each hook settings file, remove the Muse plugin with muse plugins remove threefold and delete .threefold-muse/, remove the pre-commit hook, and delete the lines it added to .git/info/exclude.


          
The older form, from a checkout of the repository

The same installer is also in the Threefold repository as scripts/threefold_install.py, and still takes the form it had before connect: a --repo path, --mode (default observe there, which caps the machine to dry runs) and --uninstall.


          

Codex reads project hooks only in a trusted project

Codex loads a project's hooks only when that project is trusted in Codex. Until you trust the repository there, .codex/hooks.json is not read and Codex's edits meet Threefold at commit time only, through the pre-commit hook.

Which agents stop the write, as measured

On 2026-09-21 a local hook answered deny and the file system was checked afterwards. Claude Code 2.1.220 refused a Write and a Bash redirection, and the Antigravity desktop app refused write_to_file; in each case the refused file was not created. Codex CLI 0.155.0 was measured on 2026-09-23, once: the hook refused an apply_patch that added boto3 to a governed file, and the file's sha256 was unchanged afterwards. Muse 1.4.0 was measured on 2026-09-28: the hook refused a write_file and the refused file was not created, and in a live session a deny stopped an edit_file and a shell command too, each file unchanged on the disk. One machine, Windows, those versions: method, results and the Codex routes that are not measured are in docs/evidence/ENFORCEMENT_2026-09-21.md, docs/evidence/ENFORCEMENT_2026-09-23.md and docs/evidence/ENFORCEMENT_2026-09-28-MUSE.md in the repository.

Install by hand: one file, four agents

One agent at a time. For a repository you mean to keep governed, the installer above writes all of this for the four agents at once and keeps it out of the repository's history.

three steps

Step 1 · Take the script

This deployment serves it. No repository, no account, no package manager: the same stack you just watched refuse a call hands you the file that asks it. The same file serves every agent; --agent tells it which one is calling.


              
            

It reads one tool call as JSON on stdin and writes at most one decision on stdout, which is the whole of its interface, so the second command above is also exactly what the agent does to it. It is served as readable text rather than as a download you cannot inspect, because a file that sits in front of an agent's tool calls is one you should read before you run.

Step 2 · Register it with your agent

In the project you want governed, add the block for your agent, with the absolute path to the script on your machine. The matcher decides which tool calls reach the hook: the ones below are writes, edits and shell commands, so reads are never sent.

Claude Code · .claude/settings.local.json


              
            

Codex · .codex/hooks.json


              
            

Antigravity · .agents/hooks.json


              
            

Muse · a plugin, installed per project


              
            

The three settings blocks share one layout. The file each agent reads, its matcher and the --agent name are what this project fixed for it; check the layout against your agent's own hook documentation, above all for Antigravity, before you rely on it. Muse reads no settings file: it runs the hook as a plugin, so instead of a block it takes the install commands above, once .threefold-muse/ holds the bundle the installer writes.

Step 3 · Tell the hook which project this is

Set these in the environment the agent is started from. Only THREEFOLD_PROJECT is required. Each one is described in the table below.


              
            

See the decision the agent would get

Idle

Each button sends the call to the endpoint set above, under a new session id, and then writes the answer the way the hook writes it for the agent you pick. Nothing here is canned: if the service is unreachable the panel says so instead of showing a verdict. The body is marked as coming from this page and asks for the model's sentence, which the hook does not, so the service's own answer below can carry a sentence the agent would not be shown.

What Claude Code receives on stdout

Press a button above.
The service's own answer
Nothing sent yet.

If a repository installs nothing

Every pull request is judged with nothing installed on any machine.

Hooks check supported writes before they run, but no hook can compel an engineer to install it. The merge can: scripts/judge_pr.py, run by .github/workflows/pr-judge.yml as a required check, sends every added or changed file through the same gates and fails the merge on a refusal or a fired rule. The judge runs from the base branch with the request as data only, so the request cannot neuter it. Anything it cannot judge fails the merge too: the gate fails closed. This repository's own pull request PR #6 proved it, red on a planted credential, then green.

What leaves your machine

What the hook sends, what it holds back, and why. Read this before Step 2.

Sent, for a call that passes the checks on the right

One POST /evaluate-tool-call with these fields and no others:

  • session_id
  • project_name: your THREEFOLD_PROJECT alias. A name that does not match the stack's pattern (Acme- and up to 40 letters, digits or dashes by default) is stored and counted as unlabelled, and the answer says so in warnings.
  • developer: anonymous, or a 12-character hex hash computed on your machine. Never a user name. Any developer value is shown in public only as a short hash.
  • tool_name, action_type and arguments. For a write, the arguments include the text being written: that text is what the gates read.
  • agent: claude-code, codex, antigravity or muse; origin: hook; explain: false, so the hook does not ask for the model's sentence.

Never sent

  • A tool call whose target does not resolve inside the project root. The service governs the project, not the rest of your disk.
  • Paths under an agent's own configuration or memory: ~/.claude, ~/.codex, ~/.gemini, ~/.local/share/muse. Those hold your instructions and history for the agent, not project code.
  • Data files, recognised by extension and by directory. The gates judge code; a data file is where records live.
  • Any call containing a term from your never-send list. You name what must not leave, in never_send.txt, and the hook checks it before it sends anything.

A call held back is not sent, so it is not checked.

Kept on your machine

The never-send list, your project aliases and the list of governed repositories live in ~/.threefold/, or wherever THREEFOLD_HOME points. They are read there by the hook. Of the three, only the alias you set for this project appears in a request, as project_name.

What each agent does with a deny, as far as it has been shown

The hook's side is fixed. The agent's side was measured on 2026-09-21, and for Codex on 2026-09-23 and Muse on 2026-09-28, on one Windows machine, for the versions named, by checking whether the refused file existed afterwards.

Agent Tool calls sent What the hook prints on a refusal Does the deny stop the write?
Claude Code Write, Edit, MultiEdit, NotebookEdit, Bash hookSpecificOutput.permissionDecision "deny", with a reason
stops the write · measured

2.1.220: a Write and a Bash redirection were refused and neither file was created. The other tools were not measured.

Codex apply_patch, Edit, Write, Bash hookSpecificOutput.permissionDecision "deny", with a reason
stops the write · measured

0.155.0, one run: the hook refused an apply_patch that added boto3 to a governed file, and the file's sha256 was unchanged afterwards. Over the shell the hook was called four times in that run and refused none, so that route is not measured, and neither is a refusal over apply_patch from a hook that makes no network call.

Antigravity write_to_file, replace_file_content, multi_replace_file_content, run_command {"decision": "deny", "reason": …}
stops the write · measured

Desktop app, 2026-09: write_to_file was refused and the file was not created. The other tools were not measured.

Muse write_file, edit_file, powershell hookSpecificOutput.permissionDecision "deny", with a reason
stops the write · measured

1.4.0: a write_file was refused and the file was not created, and in a live session an edit_file and a powershell command were refused with each file unchanged on the disk.

On an approval the hook prints nothing, for every agent, so the agent's own permission flow still runs. Until a deny has been shown to stop the write for an agent, a refusal recorded for that agent says what the agent was told, not what it did.

An edit that gets past the hook and is refused only at commit time, by the pre-commit gate, is recorded with agent pre-commit. It is reported as a commit-time refusal, never as an edit the hook stopped.

Environment

Variable Needed Effect
THREEFOLD_PROJECT required The name this project is sent under, as project_name. Use an alias in the Acme- style, never the real name: it is what the console and the metrics show.
THREEFOLD_DEVELOPER optional Who you are, for the per-developer counts. The hook hashes it on your machine and sends only the 12-character hash; unset, it sends anonymous.
THREEFOLD_FAIL_CLOSED optional Set to 1 to refuse the call when the service cannot be reached. Unset, an unreachable service means the hook prints nothing and exits 0.
THREEFOLD_HOME optional Where the hook's local files live, ~/.threefold unless set. never_send.txt goes there and holds the terms you never want sent: a call containing any of them is never sent.
THREEFOLD_ENDPOINT optional The service the hook asks. The snippets on this page set it to the endpoint set above.

It fails open, and silently

If the service times out or the network drops, the hook prints nothing and exits 0, so the agent carries on as if Threefold were not installed, and nothing on screen says the call went unchecked. That is a deliberate trade: a guard that bricks the agent on a hiccup gets uninstalled by lunchtime. Where the risk is worth the interruption, set THREEFOLD_FAIL_CLOSED=1 and the same failure refuses the call instead.

The contract, both directions

/evaluate-tool-call in the spec

Claude Code writes this on stdin

{
  "session_id": "0f3c…",
  "tool_name": "Write",
  "tool_input": {
    "file_path": "src/acme_billing/domain/invoice.py",
    "content": "from boto3 import client"
  }
}

Codex, Antigravity and Muse hand over the same call in their own tool names; --agent tells the hook which shape to read.

The hook writes this on stdout when it refuses

Claude Code, Codex and Muse:

{
  "hookSpecificOutput": {
    "hookEventName": "PreToolUse",
    "permissionDecision": "deny",
    "permissionDecisionReason": "Threefold refused this call. …"
  }
}

Antigravity:

{
  "decision": "deny",
  "reason": "Threefold refused this call. …"
}

When it approves, and when the service cannot be reached without THREEFOLD_FAIL_CLOSED=1: nothing at all, and exit 0.

Tool calls, in the service's vocabulary

Write, Edit, MultiEdit, NotebookEdit, apply_patch, write_to_file, replace_file_content, multi_replace_file_content, write_file, edit_fileFILE_WRITE
Bash, run_command, powershellCOMMAND_EXEC

Verify without an agent


              
            

Each command makes its own session id when it runs, so two people pasting the same text never share a session and one reader's third call cannot halt the session the next reader lands on. A refused call never enters the session's history, so repeating the boundary violation is refused for that reason every time and never reaches the loop gate. The last command is the one that halts a session.