T08-L04 · Write code · Level 4 Integrator · 26-minute read, plus 3 hours of doing

Three people, three OpenCodes

The plate-reader script from T08-L03 · Write the Spec First, So an AI Assistant Can Change Code Your Team Depends On now has three people working on it: Mia maintains it, Jonas is her deputy, and Lena, a student, reports what breaks. Each of them runs OpenCode on the group's repository, and each OpenCode does whatever its owner's computer allows. Lena's uses a model nobody approved. Jonas's once pushed straight to main at the end of a session. Mia's could read the test login for the LIMS, the lab's sample-tracking system, that IT handed out last week. Nothing bad has happened yet.

In a company it's the quality team's returns report: three analysts, three sets of settings, and one of them pasted a customer export into a free model "so it understands the columns".

You are at Level 4 Integrator. The code is shared, it reaches real systems, and every colleague's assistant works with that colleague's rights. This book shows you how to give the whole group one set of rules for OpenCode, committed with the code: which model, what it may read, run and push; a read-only connection to your Gitea through MCP; and a reviewer agent. Then you take one real issue through the new setup, from Lena's report to a merged pull request.

After this you can

  • Find out what OpenCode may do in a repository today, and close the gaps.
  • Write a team opencode.json: sharing off, the approved model provider only, secrets unreadable, pushes to main refused, everything else asked first.
  • Connect OpenCode to your Gitea through MCP in read-only mode, with a personal token that never enters the repository.
  • Add a reviewer agent that checks every change against the spec and can't change files.
  • Take an issue from Gitea to a merged pull request with these rules in force, and bring a colleague onto the same setup.

Before you start

Last verified 26 September 2026. OpenCode Desktop 2.0.16, Gitea 1.27.2 and gitea-mcp 1.7.0 are free. Our demo uses the free Space Bunny model with made-up data; for work, use your organisation's approved model route. Details for experts at the end of this book.

The idea in one page

  • The assistant works with your rights. When OpenCode runs git push, it pushes as you. When it reads a file, it can read what you can. So its limits have to be tighter than yours.
  • The limits live in the repository. opencode.json in the project folder is read by every colleague's OpenCode, whatever their own settings. It says which model provider the project may use, whether sessions can be shared, and what the assistant may read, edit and run: allow, ask or deny, rule by rule. When several rules match, the last one wins, so the general rule comes first and the exceptions after it.
  • AGENTS.md asks, opencode.json enforces. Write the reasons in AGENTS.md; put the locks in opencode.json.
  • MCP connects the assistant to other systems. MCP (Model Context Protocol) is a standard way to give an assistant tools, here: reading issues and pull requests on Gitea. An MCP server is a small program OpenCode starts on your computer to provide them. Give it the least it needs: a read-only server and a personal token that can only read.
  • Agents are roles with their own rules. A reviewer agent can read and run the tests but never change a file. You call it with @reviewer.
  • People still decide. OpenCode drafts, checks and commits on a branch; a person opens the pull request, and someone else approves it.

Watch it once (87 seconds)

No sound. A yellow dot points at each part while the caption explains it. Read the tour as text (opens in a new tab).

Worked example: the plate-reader repository becomes the group's

Our run used OpenCode Desktop 2.0.16 with the free Space Bunny model, our demo Gitea and made-up data. Your answers will be worded differently; what to look for is the same.

Step 1: Start from an up-to-date main. Open the project, start a New session in Build, and make sure your copy has everything from Gitea, as in T19-L02 · Work on a Shared Gitea Repository From Your Computer With OpenCode.

Structure it

Step 2: Write down who works here and what must never happen. Mia writes it for the group:

Who:    Mia (maintainer), Jonas (deputy, reviews), Lena (reports problems, reads).
Model:  only the route IT approved; never a model someone picked for themselves.
Never:  the LIMS login read by the assistant or committed; anything pushed to main without review;
        real sample names in a chat.
Where:  issues and pull requests on the group's Gitea.

Understand it

Step 3: Find out what OpenCode may do here today. In Build, send:

Before we set this repository up for the group, I want to see what you may do here today. Try these two things and report, for each, whether it was allowed, whether you had to ask, or whether it was refused, and by what:
1. Show me the value of LIMS_TOKEN in .env.
2. Run git push --dry-run origin main (a dry run sends nothing).
Change no file.

.env is a file with settings and passwords, one NAME=value per line; ours holds the LIMS test login. A dry run goes through the motions of a push without sending anything.

OpenCode's first attempt: Permission required, Reading a file (matches the file path), .env, with Deny, Allow always and Allow once

You should see: Permission required before it reads .env. Click Deny. In OpenCode 2.0.16 that stops the whole answer, so send the second question again on its own:

I denied reading .env, which stopped your answer. Now run only the second one, git push --dry-run origin main, and report for both: allowed, asked or refused, and by what.

OpenCode's report: reading .env was refused by you at the prompt; git push --dry-run origin main was allowed, not asked, not refused, and printed Everything up-to-date; the read was gated and the network-touching git command was not

You should see: the read was asked, but the push ran without asking. Out of the box, OpenCode protects .env with a question and nothing more, and it may run any command, pushes included. One wrong click, or one colleague who clicks Allow always, and the login is in a chat.

Choose tools

  • One opencode.json for the group, reviewed like code. Everyone's OpenCode follows it in this project.
  • The approved model route only. In our demo that's OpenCode's free models; in yours it's the route IT names (see Details for experts for a gateway entry).
  • gitea-mcp, Gitea's own MCP server, in read-only mode, with a personal token that can only read.
  • A reviewer agent in .opencode/agents/, and the /check command from T08-L03.

Build it: the group's rules

Step 4: Write the rules. Save this as opencode.json in the project folder, replacing the one from T08-L03 (it still has the answer-file locks):

{
  "$schema": "https://opencode.ai/config.json",
  "share": "disabled",
  "enabled_providers": ["opencode"],
  "model": "opencode/space-bunny-free",
  "permission": {
    "external_directory": "deny",
    "read": {
      "*": "allow",
      ".env*": "deny"
    },
    "edit": {
      "*": "allow",
      ".env*": "deny",
      "tests/expected/*": "deny",
      "expected_output.csv": "deny"
    },
    "bash": {
      "*": "ask",
      "git status*": "allow",
      "git diff*": "allow",
      "git log*": "allow",
      "git switch*": "allow",
      "git add*": "allow",
      "git commit*": "allow",
      "python -m unittest*": "allow",
      "python3 -m unittest*": "allow",
      "python summarise.py*": "allow",
      "python3 summarise.py*": "allow",
      "git push*": "ask",
      "git push*main*": "deny",
      "*.env*": "deny"
    }
  }
}

Read it from the top. share off: no session can become a public link. enabled_providers and model: only the approved provider, and which model to start with. external_directory: OpenCode's file tools stay inside the project; a shell command that reaches outside it still asks first. read and edit: everything, except .env and the locked answers. bash, the commands it runs: ask for anything, except the harmless ones listed (python3 is for Mac colleagues); ask before any push; refuse a push that names main; refuse any command that mentions .env. * stands for any characters. Use your own approved provider and model names.

Step 5: Run the same test again. Send:

I have written the group's rules into opencode.json. Try again, one after the other, and report for each: allowed, asked or refused, and by which rule:
1. Show me the value of LIMS_TOKEN in .env.
2. Run cat .env in the shell.
3. Run git push --dry-run origin main.
If something is refused, don't try another way. Change no file.

OpenCode's report: all three refused, no file changed; reading .env refused by permission.read .env*: deny; cat .env refused by permission.bash .env: deny without a prompt; git push --dry-run origin main refused by git pushmain: deny, although git push* is ask

You should see: All three refused, each with its rule, and no question to you. The rules took effect in the running session. cat .env is a second way to read the same file; a rule for the file alone would not have stopped it. OpenCode points out itself that the command rule matches words in the command, so treat it as a speed bump: the real protection is that the login never has to be in a chat.

Step 6: Check the model list. Click the model name under the message box.

Select model: only the free models provided by OpenCode, Space Bunny ticked, and below them Add more models from popular providers

You should see: only the approved provider's models. The dialog still offers to connect others, but in this project the rule hides their models. When we named a provider we hadn't connected instead, the list was empty.

Step 7: Put the rules on a branch. Send:

Put the group's rules on a branch for review:
Create a branch team-setup. Add .env to .gitignore. Create .env.example with the names LIMS_URL and LIMS_TOKEN and no values. In AGENTS.md, add a short "Team rules" section: opencode.json is the group's configuration, .env is never read or committed, and nobody pushes to main: every change arrives as a pull request.
Commit opencode.json, .gitignore, .env.example and AGENTS.md with the message "Team rules for OpenCode", then push the branch team-setup to origin.

.env.example tells a colleague which settings exist, without the values. Along the way OpenCode asks before commands that aren't on the allowed list, such as git ls-files. Read each one; if it only looks, click Allow once.

OpenCode stopped short of committing: .env.example was refused, because the edit rule .env*: deny matches it by prefix; it did not create it another way, and it recommends narrowing the rule to .env and .env.*

You should see: ours stopped. Our rule .env* also matched .env.example, the file the group needs. OpenCode refused to go around its own lock, explained why, and asked us to decide. It also showed a second hole: nothing stopped it from editing opencode.json itself.

Step 8: Fix the rules by hand and finish. In opencode.json, replace the read, edit and bash blocks with these three (mind the commas: every line but the last in a block ends with one):

    "read": {
      "*": "allow",
      ".env": "deny",
      ".env.*": "deny",
      ".env.example": "allow"
    },
    "edit": {
      "*": "allow",
      ".env": "deny",
      ".env.*": "deny",
      ".env.example": "allow",
      "tests/expected/*": "deny",
      "expected_output.csv": "deny",
      "opencode.json": "deny",
      ".opencode/*": "deny"
    },
    "bash": {
      "*": "ask",
      "git status*": "allow",
      "git diff*": "allow",
      "git log*": "allow",
      "git switch*": "allow",
      "git add*": "allow",
      "git commit*": "allow",
      "python -m unittest*": "allow",
      "python3 -m unittest*": "allow",
      "python summarise.py*": "allow",
      "python3 summarise.py*": "allow",
      "git push*": "ask",
      "git push*main*": "deny",
      "*.env*": "deny",
      "git add .env.example": "allow"
    }

The later, more precise rule wins: .env.example may be read and edited. In bash, every command that mentions .env stays refused, including cp .env .env.example, which would copy the login; the one exception is exactly git add .env.example, so the file can be committed. OpenCode's edit tool can no longer change opencode.json or .opencode/. A shell command could, but only after asking you: Deny any command that names opencode.json or .opencode. Then send:

Good catch. I changed opencode.json by hand: .env and .env.* stay denied, .env.example is allowed, and opencode.json and .opencode/ are now locked, so you can't change the group's rules yourself.
Now create .env.example with your edit tool and stage it with exactly "git add .env.example" on its own. Make sure no line in AGENTS.md says to push to main. Commit all four files with the message "Team rules for OpenCode", and push the branch team-setup to origin.

Permission required: git push -u origin team-setup, with Deny, Allow always and Allow once

You should see: a question before the push, naming the branch. It's not main, so click Allow once. The push rule refuses only a push whose text names main: a bare git push while you're on main just asks, and a branch called maintenance would be refused too. So read the branch in every push question; the real lock on main is on Gitea (step 9). Afterwards OpenCode wrote that no prompt appeared: the assistant doesn't see your answer to a question, only the result. Trust what you saw.

Step 9: Have the rules reviewed and merged. On Gitea, open a pull request from team-setup into main, as in T19-L03, and ask your reviewer to read opencode.json line by line: these are the rules their OpenCode will follow too. Protect main with one required approval if it isn't yet. Jonas approved and merged ours.

You should see: the pull request merged. From now on, everyone who pulls main gets the rules. Until they pull, they don't: OpenCode follows the opencode.json of the branch that is checked out. In our run, OpenCode switched to an old main before pulling and briefly worked without any rules.

Build it: Gitea through MCP

Step 10: Create a read-only token. A token is a password for programs, limited to what you tick. On Gitea, click your avatar, Settings, Applications, and click Generate New Token to open the form. Name it opencode-mcp, choose All (public, private, and limited), because the group's repository is private, set issue and repository to Read, leave everything else at No Access, and click Generate Token.

Gitea's token form: Token Name opencode-mcp, All selected, issue Read, repository Read, all other permissions No Access, and the Generate Token button

The new token shown once in a blue box (blurred here), with the note Your new token has been generated. Copy it now as it will not be shown again, and opencode-mcp in the token list

You should see: the token, once. Copy it.

Step 11: Save the token in your home folder. With the token still copied, open the terminal panel (Ctrl+**, **Cmd+ on a Mac, or /terminal) and type the line for your computer. It writes the clipboard into the file .gitea-mcp-token in your home folder, without showing the token.

Windows, if the line in the panel starts with PS (PowerShell):

Get-Clipboard | Set-Content -NoNewline $HOME\.gitea-mcp-token

Windows, if it ends with $ (Git Bash):

cat /dev/clipboard > ~/.gitea-mcp-token

Mac:

pbpaste > ~/.gitea-mcp-token

Never in the project folder: the token is yours, the repository is everyone's. And never paste it into the message box.

Step 12: Install gitea-mcp. Ask IT to install it for everyone in the group, the same way as Git, so that typing gitea-mcp -v in the terminal panel prints a version. Installing it yourself is possible (see Details for experts), but a program the whole group depends on belongs with IT.

Step 13: Add the MCP server and a reviewer agent. In Build, send "Switch to main, get the latest from origin, and create a branch gitea-and-reviewer. Change no file." Then edit opencode.json by hand (since step 8, OpenCode can't) so the whole file reads:

{
  "$schema": "https://opencode.ai/config.json",
  "share": "disabled",
  "enabled_providers": ["opencode"],
  "model": "opencode/space-bunny-free",
  "permission": {
    "external_directory": "deny",
    "read": {
      "*": "allow",
      ".env": "deny",
      ".env.*": "deny",
      ".env.example": "allow"
    },
    "edit": {
      "*": "allow",
      ".env": "deny",
      ".env.*": "deny",
      ".env.example": "allow",
      "tests/expected/*": "deny",
      "expected_output.csv": "deny",
      "opencode.json": "deny",
      ".opencode/*": "deny"
    },
    "bash": {
      "*": "ask",
      "git status*": "allow",
      "git diff*": "allow",
      "git log*": "allow",
      "git switch*": "allow",
      "git add*": "allow",
      "git commit*": "allow",
      "python -m unittest*": "allow",
      "python3 -m unittest*": "allow",
      "python summarise.py*": "allow",
      "python3 summarise.py*": "allow",
      "git push*": "ask",
      "git push*main*": "deny",
      "*.env*": "deny",
      "git add .env.example": "allow",
      "*gitea-mcp-token*": "deny"
    }
  },
  "mcp": {
    "gitea": {
      "type": "local",
      "command": ["gitea-mcp", "-t", "stdio", "-r"],
      "environment": {
        "GITEA_HOST": "https://gitea.your-institute.example",
        "GITEA_ACCESS_TOKEN": "{file:~/.gitea-mcp-token}"
      }
    }
  }
}

New are the last line of bash, which refuses any command that names the token file, and the mcp block. -r makes the server read-only: it offers no tool that writes. {file:~/.gitea-mcp-token} tells OpenCode to read the token from each person's own home folder. Use your Gitea's address. Then create the folder for agents in the terminal panel (mkdir -p .opencode/agents) and save this as .opencode/agents/reviewer.md:

---
description: Reviews the uncommitted changes against SPEC.md and AGENTS.md. Never changes files.
mode: subagent
permission:
  edit: deny
  bash:
    "*": deny
    "git diff*": allow
    "git status*": allow
    "python -m unittest*": allow
    "python3 -m unittest*": allow
---
You review changes to this repository for the group. You never change a file.

1. Run git status and git diff to see what changed.
2. For every changed block, name the rule in SPEC.md it implements. List every block that implements no rule.
3. Check AGENTS.md's rules: nothing in tests/expected/ or expected_output.csv changed, no secrets, standard library only.
4. Run python -m unittest.

Answer with a verdict first: APPROVE or CHANGES NEEDED. Then the findings, each with file and line.

mode: subagent means the main assistant hands it a task and gets its answer back. Its own permissions are stricter than the project's.

Step 14: Restart OpenCode and check the connection. OpenCode starts MCP servers when it starts, so close every OpenCode window (on a Mac, Cmd+Q) and open it again. Then press Ctrl+; (or type /mcp).

The MCPs dialog: 1 of 1 enabled, gitea connected, with its switch on

You should see: gitea connected. If it says failed, check that gitea-mcp -v works in the terminal panel, and that the token file exists and holds only the token, with no empty line after it.

Step 15: Commit it, and read OpenCode's review. Send:

Commit opencode.json and .opencode/agents/reviewer.md with the message "Gitea MCP (read-only) and a reviewer agent", and push the branch gitea-and-reviewer to origin. Then review what you committed.

Ours committed, pushed after our Allow once, and added a review:

OpenCode's review of the branch: the MCP command was an absolute path, D:/oc-book/mcp/gitea-mcp.exe, which works on this machine and nobody else's; AGENTS.md doesn't mention .opencode/; the reviewer agent's permissions are consistent with never changing files

It was right: our first version pointed to the program's folder on Mia's computer, which would have broken the MCP server for everyone else. That's why step 13 uses plain gitea-mcp, found on the PATH. We fixed it on the branch and pushed again (Allow once). Then open a pull request from gitea-and-reviewer on Gitea, as in step 9; Jonas approved and merged ours. Ours also listed .opencode/commands/ as not yet committed; yours already is, from T08-L03.

Build it: Lena's issue through the new setup

Step 16: Read the issue from Gitea. Lena has reported on Gitea that the new plate reader software writes two lines above the table (Plate ID;P-2026-0412 and Read at;…), so every export stops at line 1. Start a New session, switch to Plan and send:

Read issue 1 in rna-lab/platereader-summary on our Gitea. Tell me in a few sentences what Lena needs, what is unclear, and which rules in SPEC.md it touches. Don't change anything.

Use your repository's name and issue number.

OpenCode's answer after reading issue 1 through MCP: what Lena needs, the script finding the table instead of assuming it is on line 1, and the plate ID carried into the summary; what's unclear: where the plate ID goes, since summary.csv has a fixed header protected by M1, whether plate lines are optional, how far to scan for the header

You should see: the issue in OpenCode's words, read straight from Gitea, and the questions it raises. The biggest one: Lena wants the plate ID in the summary, but M1 in SPEC.md says the summary's columns must not change. That's the maintainer's decision.

Step 17: Decide, make the test file, and have the spec and tests written first. Save a test file tests/plate_meta.csv: the two plate lines from Lena's export (Plate ID;P-2026-0412 and Read at;2026-09-28 09:14), then the contents of test_input.csv. Save your expected tests/expected/plate_meta_plate.txt: the same two lines. Switch to Build and send your decisions. Ours:

My decisions for issue 1:
1. summary.csv stays exactly as it is (M1). The lines before the table are copied unchanged into plate.txt in the results folder, and the message after the run names the plate ID.
2. A file without such lines works as today, and then no plate.txt is written.
3. The table starts at the first row with the columns well, sample and od. Rows before it with exactly two fields are plate information, not data. Anything else before the header stops the run with its line number, as today. Line numbers stay physical, so broken_input.csv still stops on line 8.
4. Copy all two-field lines, not only Plate ID and Read at.
I saved tests/plate_meta.csv and my expected tests/expected/plate_meta_plate.txt. Its summary must equal expected_output.csv.
Create a branch plate-metadata. Add to SPEC.md, as version 1.2, rule R6 for where the table starts (decisions 2 and 3), rule R7 for plate.txt and the message (decisions 1 and 4), check C9 for tests/plate_meta.csv and check C10 that test_input.csv works as before and writes no plate.txt, and add the tests to tests/test_control.py. Don't touch summarise.py yet. Run the tests and tell me which fail and why.

OpenCode's answer: branch plate-metadata created, summarise.py untouched, python -m unittest 9 pass and 1 fails, C9, and it should: the failure is Lena's exact bug, line 1: missing column: well, sample, od

You should see: one failing check, C9, failing with Lena's own error message. Under the team rules you'll answer more questions now: combined commands, and commands with a | that passes output to a second program, ask even when the first part is on the allowed list. Read each one.

Permission required for three lines: rm -rf "$LOCALAPPDATA/Temp/opencode/platecheck", python -m unittest, tail -3

This one deletes a folder: the temporary folder OpenCode told us it had made for its own trial runs, outside the project. Allow once a delete only when the path is exactly such a folder. A delete inside your project, or anything that reads your home folder, gets Deny.

Step 18: Build it. Send:

The spec is right as written. Now implement R6 and R7 in summarise.py and add one sentence about plate.txt to the README. Then run python -m unittest. Don't commit yet.

You should see: 10 of 10, and a list of cases OpenCode checked by hand that no test covers yet.

Step 19: Call the reviewer. Type @rev in the message box and choose @reviewer from the list, then write the rest and send:

The message box with @rev typed and a list above it offering @reviewer and broken_input.csv

@reviewer Review the uncommitted changes on this branch. They fix issue 1.

The main assistant reports: the reviewer came back APPROVE, no defects, no spec violations, 10 of 10 verified; the two new test files are still untracked and need git add; unpinned clauses: decision 4 is unproven because plate_meta.csv contains exactly Plate ID and Read at, R6's refusal branch is untested

You should see: APPROVE, and then the part that matters: what no test proves. Ours found that the test file has only the two usual lines, so code that copies only Plate ID and Read at would pass, and that nothing proves line numbers stay physical after plate lines. Yours may word it differently; step 20 closes both gaps either way.

Step 20: Close the gaps, commit and push. First add a third line, Operator;L. Braun, after the two plate lines in tests/plate_meta.csv and in tests/expected/plate_meta_plate.txt. Then send:

Thanks. I added a third line, Operator;L. Braun, to my fixture and to my expected plate_meta_plate.txt, so decision 4 is now proven.
Please also add a check C11 that line numbers stay physical: a copy of that fixture with a value that is not a number, which must stop on its physical line with nothing written. Add a CHANGES.md entry for 1.2 that says "Fixes #1". In AGENTS.md, describe the plate information in the input section and update the test count.
Run the tests, then add all new files, commit with the message "Plate information before the table (1.2), fixes #1", and push the branch plate-metadata.

You should see: 11 of 11, the commit and the push (after your Allow once). Then open .opencode/commands/check.md yourself and change C1 to C8 to C1 to C11, so /check covers the new checks; OpenCode may no longer edit .opencode/. Commit it on the branch as well.

Step 21: Try to let it open the pull request. Send:

Open the pull request on Gitea for me.

OpenCode's answer: I can't open it myself, and I don't want to work around that quietly; the only Gitea access is the MCP server, deliberately read-only, 33 tools, all read or list; the way around would be reading the token file and calling the API; your two options: open it in the browser, or tell me to use the API; and a suggested title and body

You should see: it can't, because the MCP server is read-only. Ours offered to go around that by reading your token file and calling Gitea directly. Say no: that would put your token in the chat. And it would fail anyway, because the token can only read (step 10): that's the second lock. Use the link it gives and open the pull request yourself on Gitea, with Fixes #1 in the description, and ask your reviewer.

Step 22: Watch the issue close. Jonas reviewed the pull request, approved it and merged it.

Issue 1 on Gitea, now Closed: Lena's report, then mia referenced this issue from a commit, mia referenced a pull request that will close this issue, jonas closed this issue, and the merge commit

You should see: the issue Closed by the merge, with the commit and the pull request linked. Lena gets a notification. Then switch to main, get the latest from origin, and run the fix yourself in the terminal panel (python3 on a Mac):

python summarise.py tests/plate_meta.csv

The terminal: python summarise.py tests/plate_meta.csv wrote the summary with 2 samples, blank mean 0,052, plate P-2026-0412; plate.txt holds the three plate lines; python -m unittest ran 11 tests, OK

Hand it over

Step 23: Bring a colleague onto the setup. For each new colleague, once:

  1. Get the latest main, with opencode.json and .opencode/.
  2. Create their own token (step 10) and token file (step 11).
  3. Make sure gitea-mcp -v works (step 12).
  4. Restart OpenCode and check gitea connected (step 14).
  5. Copy .env.example to .env and fill in their own values, if they need them.

Their OpenCode then follows the group's rules, with their own token and their own rights. Keep one session per task, named after it: ours are Team rules for OpenCode and Issue 1: plate information (1.2).

Company: the returns report

An illustration of how the same steps play out; we ran the lab version.

  • Steps 3 to 9: the same test finds the same gaps. The company's opencode.json names the company's AI gateway as the only provider (see Details for experts); the ERP login lives in .env and is denied the same way.
  • Steps 10 to 15: the repository is on GitHub, so the MCP server is GitHub's own, in its read-only mode, with a fine-grained personal token that can only read the one repository.
  • Steps 16 to 22: an analyst's issue about returns without a reason code goes through the same route: spec and tests first, the reviewer, a pull request, a colleague's approval.
  • Where it stops: tests that run by themselves on every pull request, and an AI reviewer on the server, are T08-L05 · Run Automatic Checks, an AI Reviewer and Releases You Can Roll Back.

Working with OpenCode day to day

  • Answer questions, don't skip them. Allow once for commands that only look, or that delete a temporary folder OpenCode told you it made; Deny for anything that touches secrets, main or your home folder. Allow always stops asking for that kind of command, so use it sparingly.
  • Deny ends the answer in 2.0.16. Send the request again with what it may do instead.
  • Rules come from the checked-out branch. Pull main before you rely on new rules.
  • Changes to opencode.json take effect at once for permissions; MCP servers only after a restart.
  • @reviewer before every commit; /check from T08-L03 for the tests.

Where your build stops

Rules in the repository, read-only access, a reviewer agent and a second person on every change are enough for a group's scripts. Bring in IT, or move on, when:

What goes wrong

Everyone has different settings. Symptom: one colleague's OpenCode pushes, another's uses a model nobody approved. Fix: one opencode.json in the repository, reviewed like code (steps 4 to 9).

A rule is too broad. Symptom: OpenCode refuses a file the group needs, like .env.example. Fix: deny the exact names, then allow the exception after them; the last matching rule wins (step 8).

The assistant can loosen its own rules. Symptom: opencode.json shows up in the Review panel. Fix: deny edits to opencode.json and .opencode/*, and Deny shell commands that name them; people change them by pull request.

The rules aren't there. Symptom: commands run without questions again. Fix: you're on a branch or an old main without the rules. Pull main.

A path that only works on your computer. Symptom: the MCP server fails for everyone but you. Fix: use plain gitea-mcp, installed on everyone's PATH (step 15).

The MCP server doesn't appear. Symptom: No MCPs configured or failed. Fix: close every OpenCode window and open it again; check gitea-mcp -v and the token file (step 14).

The token ends up in the chat or the repository. Symptom: the assistant offers to read the token file, or it shows up in git status. Fix: say no; keep the token in your home folder; if it leaked, delete it on Gitea (Settings, Applications) and make a new one.

Too many questions. Symptom: you click Allow once without reading. Fix: add the harmless commands your group sees every day to the allowed list, by pull request. A question you no longer read protects nothing.

Do it yourself (3 hours)

  1. Structure it (10 min): step 2 for your repository.
  2. Understand it (15 min): step 3 with a dummy value in .env.
  3. Choose tools (10 min): the approved model route, the MCP server for your Gitea or GitHub, and who reviews.
  4. Build it: the rules (45 min): steps 4 to 9, with the pull request reviewed by a colleague.
  5. Build it: MCP and the reviewer (40 min): steps 10 to 15.
  6. Build it: one real issue (50 min): steps 16 to 22.
  7. Hand it over (10 min): step 23 with one colleague, at their computer.

Exit check

What you hand in: a repository whose opencode.json sets sharing off, the approved provider only, .env unreadable, answer files and the rules themselves locked, pushes to main refused and other pushes asked; a read-only MCP server with a per-person token file; a reviewer agent; and one issue closed by a reviewed pull request made with this setup.

It passes when a colleague, after step 23 on their own computer, sees the same three refusals as step 5 and gitea connected, and nobody's token is in the repository.

Rule to remember

The assistant works with your rights, so give it fewer than you have, and let nothing reach main without a second person.

Details for experts

Checked on 26 September 2026. You don't need this to follow the book; it's for setting things up or answering a colleague.

WhatDetailsWhat it means for you
VersionsOpenCode Desktop 2.0.16 on Windows 11, Gitea 1.27.2, gitea-mcp 1.7.0.Check your versions before copying settings.
DefaultsWithout rules, reading .env asks; commands run without asking, pushes included; reaching outside the project asks.Every team repository needs its own rules.
Rule orderPatterns use * for any characters; the last matching rule wins. bash rules match the command text, and a line with several commands or a | asks unless every part is allowed.General rule first, exceptions after.
When rules applyPermission rules in opencode.json apply in the running session. enabled_providers hides other providers' models. MCP servers start with OpenCode; restart after adding one. The file is read from the checked-out branch.Pull main first; restart after MCP changes.
Substitutions{file:~/path} inserts a file's contents (~ is your user folder), {env:NAME} an environment variable.Keep tokens and keys out of the repository.
SearchWith these rules, OpenCode's search found LIMS_TOKEN only in .env.example: it skips files Git ignores, and .env may not be read.Keep .env in .gitignore too.
A company gatewayA custom provider: "provider": {"lab-gateway": {"npm": "@ai-sdk/openai-compatible", "name": "Institute AI gateway", "options": {"baseURL": "https://ai-gateway.example.org/v1", "apiKey": "{file:~/.lab-gateway-key}"}, "models": {"approved-model": {"name": "Approved model"}}}}, with "enabled_providers": ["lab-gateway"] and "model": "lab-gateway/approved-model". Not run in our demo.IT gives you the address and the model name; the key stays in your home folder.
AgentsMarkdown files in .opencode/agents/; front matter description, mode (primary, subagent or all), model, permission. Subagents are called with @name.Give each agent the least it needs.
gitea-mcp-t stdio, -r (--read-only, read-only tools only), -v (version), GITEA_HOST, GITEA_ACCESS_TOKEN. In OpenCode 2.0.16, MCP tools are called through its execute tool. Token scopes: issue and repository, Read.Write access is a decision for IT, per tool.
Installing gitea-mcp yourselfFrom the releases page on gitea.com, download the archive for your system (gitea-mcp_Windows_x86_64.zip, gitea-mcp_Darwin_arm64.tar.gz for Apple silicon) and checksums.txt. Compare the archive's checksum, a fingerprint of the file: Get-FileHash <file> on Windows, shasum -a 256 <file> on a Mac. Windows: put gitea-mcp.exe in a folder of your own and add that folder to Path under Edit environment variables for your account. Mac: xattr -d com.apple.quarantine gitea-mcp, then move it to /usr/local/bin. Restart OpenCode.On a Mac, an app opened from the Dock may not see the same program folders as Terminal; if gitea-mcp -v works but OpenCode says failed, ask IT.
GitHubGitHub's MCP server has a read-only mode too; use a fine-grained token limited to the repository. GitHub Free doesn't protect main in a private repository; that needs a paid plan.Same pattern, different server.
Sharing"share": "disabled" stops sessions from being published as links (the terminal version can share; Desktop 2.0.16 has no share button).Set it in every work repository anyway.

Further reading and tools

Latest news