Run a Local Coding Agent Safely
Run a permission-gated local agent to fix and test a Python script.
Introduction
30 Second Summary
When software can edit files or run terminal commands, every hidden action carries risk. Clear boundaries make that power easier to trust.
In this project, you will connect OpenCode to the qwen3:4b-thinking model running on your Mac through Ollama. You will contain the agent inside a permission-gated Python workspace while it diagnoses a real case-sensitivity bug under your review.
What You'll Build
Demo a local terminal agent that pauses for your approval throughout a tested SQL bug fix with rollback.
By the end of this project, you'll have:
- A local coding agent that answers through qwen3:4b-thinking running on your Mac.
- A permission-gated sandbox that asks before file edits or shell commands while denying access outside ~/local-agent-lab.
- A tested SQL inventory that finds .sql plus .SQL files while excluding .txt files.
- Secret Mission: Practice controlled rollback by approving a harmless output-word regression before restoring the tested script with /undo.
Are there any prerequisites?
Your Mac needs macOS 15 with Visual Studio Code plus Python 3.8 or newer.
Keep at least 3 GB of disk space free with enough memory to run a 4B local model.
Before We Start
Before an AI agent sees your files, decide what it can access and which actions need your approval. This is your moment to commit to a local OpenCode sandbox that inventories SQL files without sending your code or prompts to a cloud model.
Install the Local Agent Tools
Your sandbox can only stay local when its answers come from local inference. The OpenCode agent needs a running Ollama service before it can work with your future workspace.
This step verifies your Python installation. It then prepares the local model stack without a cloud API key.
In this step, get ready to:
- Verify that Python meets the project requirement.
- Install Ollama and download the local model.
- Install OpenCode through the Ollama integration.
Verify Python
Python runs the SQL inventory script later in the project. This project requires Python 3.8 or newer.
- Press Cmd+Space to open macOS search.
- Type Terminal into the search bar.
- Press Enter to open Terminal.
- Check your installed Python version by running this command:
python3 --version
What does this command confirm?
The python3 --version command asks the macOS Python executable to print its version. You are ready when the result is Python 3.8 or newer.
Python version too old?
Update Python through the installation method you already use on your Mac. Run the version check again after the update.
If the command does not return a version, help me verify Python on macOS.
Install Ollama and the model
Ollama runs the model directly on your Mac. Its context length must reach at least 64K for this agent workflow.
- Click Finder in the Dock.
- Select Applications in the Finder sidebar.
- Look for Ollama in the application list.
- Select Ollama if it is present.
- Press Cmd+I to check its installed version.
✔️ I have the required Ollama version
Ollama 0.40.2 is already installed. Start the app so its local service is available.
- Press Cmd+Space to open macOS search.
- Type Ollama into the search bar.
- Press Enter to start Ollama.
ⓧ I have an older Ollama version
Your installed copy needs an update to Ollama 0.40.2 before you continue.
- Open the official Ollama macOS guide in your browser.
- Download the macOS installer.
- Open ollama.dmg from your Downloads folder.
- Drag Ollama into the system-wide Applications folder.
- Confirm the replacement if macOS asks.
- Press Cmd+Space to open macOS search.
- Type Ollama into the search bar.
- Press Enter to start the updated app.
- Allow Ollama to create its command-line link if prompted.
ⓧ Ollama is not installed
Install Ollama 0.40.2 from its official macOS download.
- Open the official Ollama macOS guide in your browser.
- Download the macOS installer.
- Open ollama.dmg from your Downloads folder.
- Drag Ollama into the system-wide Applications folder.
- Press Cmd+Space to open macOS search.
- Type Ollama into the search bar.
- Press Enter to start Ollama.
- Allow Ollama to create its command-line link if prompted.
The local model needs enough room to process code plus agent instructions. Set the context limit before downloading the model.
- Click the Ollama icon in the macOS menu bar.
- Select Settings from the app menu.
- Move the context-length slider to at least 64K.
The model download transfers about 2.5 GB. Allow several minutes for it to finish on a typical internet connection.
- Switch back to Terminal.
- Download qwen3:4b-thinking by running this command:
ollama pull qwen3:4b-thinking
What does this command do?
The ollama pull command downloads the selected model into Ollama's local model store. The files stay on your Mac for future sessions.
- Confirm that Ollama stored the model by running this command:
ollama ls
What does this command show?
The ollama ls command lists the models stored by Ollama. The output should contain qwen3:4b-thinking with a size of about 2.5 GB.
That is the largest download complete. Your Mac can now serve the project model locally.
Model missing from the list?
Confirm that Ollama is still running in the macOS menu bar. Run the download command again if it stopped before completing.
If the download still fails, help me troubleshoot the Ollama model download.
Install and verify OpenCode
OpenCode gives the local model an agent interface inside Terminal. Ollama provides an integration that configures the local connection without starting an interactive session.
The configuration command can show a model chooser. It can also offer to install OpenCode when the executable is missing.
- Return to Terminal from earlier.
- Prepare to select qwen3:4b-thinking when the model chooser appears.
- Prepare to accept the OpenCode installation prompt if it appears.
- Configure OpenCode for the local Ollama model by running this command:
ollama launch opencode --config
What does this command do?
The ollama launch opencode --config command configures OpenCode to use Ollama. The --config flag completes the configuration without opening an interactive agent session.
- Check the installed OpenCode version by running this command:
opencode --version
What should the version check show?
The command prints the installed OpenCode version. The required result for this project is 1.18.35.
✔️ I have the required OpenCode version
OpenCode 1.18.35 is installed. The Ollama integration is ready to provide its local model connection.
ⓧ I see an older OpenCode version
Update OpenCode with its official installer. Your Ollama provider configuration remains available after the executable updates.
- Install the current OpenCode release by running this command:
curl -fsSL https://opencode.ai/install | bash
What does this installer do?
The command downloads OpenCode from its official installer. It updates the executable used by Terminal.
- Confirm the updated version by running this command:
opencode --version
What should change?
The version output should now report 1.18.35.
ⓧ Command not found
The Ollama configuration may not have completed the OpenCode installation. Repeat the integration command so you can accept its installation prompt.
- Restart the Ollama installation workflow by running this command:
ollama launch opencode --config
What happens this time?
The integration checks for OpenCode again. Accept its installation prompt when it appears.
- Verify the new executable by running this command:
opencode --version
What confirms success?
Terminal should recognize the command. The output should report 1.18.35.
This setup uses the Ollama provider on your Mac. You never enter a cloud API key.
OpenCode still unavailable?
Confirm that the Ollama app is running before repeating the configuration command. Restart Terminal if the installer finished but the version command remains unavailable.
If OpenCode still cannot start, help me diagnose the local OpenCode installation.
Before the final check, do you expect all three commands to confirm the local stack you just prepared?
- Recheck Python by running this command:
python3 --version
What does this final Python check prove?
The result proves that the interpreter needed by the inventory script is available in Terminal.
- Recheck the local model by running this command:
ollama ls
What does this final model check prove?
The result proves that Ollama has stored qwen3:4b-thinking locally.
- Recheck OpenCode by running this command:
opencode --version
What does this final agent check prove?
The result proves that Terminal can find the installed OpenCode executable.
You should see Python 3.8 or newer.
You should see qwen3:4b-thinking in the Ollama model list.
You should see OpenCode version 1.18.35.
Your local agent stack is ready. The runtime, model, and agent executable now work without a cloud API key.
Your tools are in place. Next, you will give the agent a workspace with explicit boundaries and approval gates.
Build the Permission-Gated Sandbox
Your local Ollama model is ready. OpenCode is installed too.
OpenCode permits most actions by default, so this workspace needs explicit approval gates before the agent sees code. A project-level opencode.json file creates a narrow workspace boundary for the agent.
In this step, get ready to:
- Create an isolated workspace with starter input files.
- Add the starter script under a project-level permission policy.
- Verify the policy with the local agent.
Create the workspace and starter inputs
The local-agent-lab workspace keeps the agent focused on one small project. Its input folder provides a SQL file to find plus an unrelated text file to ignore.
- Use macOS search to find Visual Studio Code.
- Select Visual Studio Code from the search results.
- Click File in the menu bar.
- Click Open Folder....
- Select your macOS home folder in the folder picker.
The folder picker is now pointing at the location represented by ~ in terminal paths.
- Click New Folder.
- Enter local-agent-lab as the folder name.
- Press Enter.
- Select the new local-agent-lab folder.
- Click Open.
You should see local-agent-lab at the top of the Explorer view. This confirms that ~/local-agent-lab is open as your workspace.
- Click the New Folder button in the Explorer view.
- Enter input as the folder name.
- Press Enter.
- Select the input folder.
- Click the New File... button.
- Enter first.sql as the file name.
- Press Enter.
- Add the first sample query by pasting this code:
SELECT 1;
What does this query do?
This query returns the number 1 when a SQL engine executes it. Here it gives the inventory script a genuine .sql file to discover.
- Save input/first.sql.
- Confirm that first.sql appears inside input in the Explorer view.
Is first.sql in the wrong place?
Drag first.sql onto the input folder in the Explorer view. The file should become indented beneath the folder.
Ask for help if the file remains at the workspace level: Help me move first.sql into the input folder in Visual Studio Code.
- Select the input folder again.
- Click the New File... button.
- Enter notes.txt as the file name.
- Press Enter.
- Add the unrelated sample text by pasting this content:
This file should not appear in the SQL inventory.
Why add a text file?
The text file gives the inventory script something to reject. A useful inventory includes matching files without collecting every file in the folder.
- Save input/notes.txt.
- Confirm that input now contains first.sql plus notes.txt.
Don't see both input files?
Expand the input folder using the disclosure control beside its name. Check that each filename matches the spelling above.
Ask for help checking the folder structure: Help me verify the input folder in my local-agent-lab workspace.
Add the starter script and permission policy
The starter script inventories matching files in input. The policy file controls what the agent can do while inspecting that script.
- Select local-agent-lab at the top of the Explorer view.
- Click the New File... button.
- Enter sql_inventory.py as the file name.
- Press Enter.
- Build the file-matching function by pasting this code:
from pathlib import Path
def find_sql_files(folder):
return sorted(
(
path
for path in folder.iterdir()
if path.is_file() and path.suffix == ".sql"
),
key=lambda path: path.name.lower(),
)
What does this code do?
- Path gives the script tools for working with folders plus filenames.
- find_sql_files() examines each entry in the supplied folder.
- path.is_file() excludes folders from the results.
- path.suffix == ".sql" keeps files with the expected suffix.
- sorted() produces a predictable filename order.
- Save sql_inventory.py.
- Confirm that sql_inventory.py appears beside the input folder in the Explorer view.
Seeing indentation warnings?
Check that the generator lines remain nested inside both parentheses. Check that the closing parentheses align with their opening statements.
Ask for help comparing the indentation: Help me check the indentation in find_sql_files().
- Place the cursor on a new line below the closing parenthesis of find_sql_files().
- Complete the starter module by pasting this code:
def main():
input_folder = Path("input")
if not input_folder.exists():
print("Input folder not found: input")
return
sql_files = find_sql_files(input_folder)
if not sql_files:
print("No .sql files found.")
return
print(f"Found {len(sql_files)} SQL file(s):")
for path in sql_files:
print(f"- {path.name}")
if __name__ == "__main__":
main()
How does the module finish the inventory?
- main() points the script at the input folder.
- input_folder.exists() catches a missing input folder.
- sql_files holds the sorted matches returned by find_sql_files().
- The final loop prints one matching filename per line.
- The module guard calls main() when Python runs this file directly.
- Save sql_inventory.py.
- Confirm that the editor shows main() below find_sql_files().
Does main() appear inside the first function?
Move def main(): fully to the left edge of the editor. Keep the statements belonging to main() indented beneath it.
Ask for help checking the function boundary: Help me separate main() from find_sql_files().
The permission policy uses JSON to assign an outcome to each OpenCode capability. The ask outcome creates an approval prompt before an action can proceed.
- Select local-agent-lab at the top of the Explorer view.
- Click the New File... button.
- Enter opencode.json as the file name.
- Press Enter.
- Define the sandbox policy by pasting this configuration:
{
"$schema": "https://opencode.ai/config.json",
"share": "disabled",
"permission": {
"read": "allow",
"edit": "ask",
"bash": "ask",
"external_directory": "deny",
"webfetch": "deny",
"websearch": "deny"
}
}
How does this policy protect the workspace?
- share disables session sharing.
- read lets the agent inspect files inside the workspace.
- edit requires approval before file changes.
- bash requires approval before shell commands.
- external_directory blocks access beyond the directory where OpenCode starts.
- webfetch blocks URL retrieval.
- websearch blocks web searches.
- Save opencode.json.
- Confirm that the Explorer view lists opencode.json beside sql_inventory.py.
Seeing JSON errors?
Check every quoted key for a following colon. Check the comma after each entry except the final entry in each object.
Ask for help comparing the policy: Help me find the JSON syntax problem in opencode.json.
✔️ Awesome, I've got everything!
Your four project files are saved in ~/local-agent-lab.
ⓧ I'd like to double check the full code
Compare each saved file with these complete versions.
SELECT 1;
This file should not appear in the SQL inventory.
{
"$schema": "https://opencode.ai/config.json",
"share": "disabled",
"permission": {
"read": "allow",
"edit": "ask",
"bash": "ask",
"external_directory": "deny",
"webfetch": "deny",
"websearch": "deny"
}
}
from pathlib import Path
def find_sql_files(folder):
return sorted(
(
path
for path in folder.iterdir()
if path.is_file() and path.suffix == ".sql"
),
key=lambda path: path.name.lower(),
)
def main():
input_folder = Path("input")
if not input_folder.exists():
print("Input folder not found: input")
return
sql_files = find_sql_files(input_folder)
if not sql_files:
print("No .sql files found.")
return
print(f"Found {len(sql_files)} SQL file(s):")
for path in sql_files:
print(f"- {path.name}")
if __name__ == "__main__":
main()
Verify the sandbox with OpenCode
A configuration file only protects the agent when OpenCode actually loads it. The resolved configuration shows the combined settings that the current workspace applies.
- Click View in the Visual Studio Code menu bar.
- Click Terminal.
Before you run the check, which permissions do you expect to require approval?
- Print the resolved OpenCode configuration by running this command:
opencode debug config
What does this command prove?
The command prints the configuration that OpenCode resolves for ~/local-agent-lab. This confirms that the project-level policy is active before the agent receives a task.
You should see sharing disabled. You should also see read set to allow.
The edit plus bash permissions should show ask. The external-directory plus web permissions should show deny.
Don't see the project permissions?
Confirm that the terminal prompt points to local-agent-lab. Confirm that opencode.json is saved at the same level as sql_inventory.py.
Ask for help tracing the configuration: Help me work out why OpenCode is not resolving my project permissions.
The resolved policy is active. Your next check asks the local model to describe that policy without using editing or command tools.
- Launch OpenCode through the local Ollama integration by running this command:
ollama launch opencode
What does this command do?
Ollama starts OpenCode with the local model configuration created earlier. The interactive agent opens inside the current local-agent-lab workspace.
Before you send the prompt, do you expect a read-only request to trigger an edit or command approval?
- Ask the agent to inspect the policy without changing or executing anything by sending this prompt:
Read opencode.json and list the permissions that require approval or are denied. Do not edit files or run commands.
Why constrain the prompt?
- The first sentence limits the task to reading one known file.
- The second sentence asks for a summary of the approval plus denial boundaries.
- The final sentence explicitly rules out edits.
- The final sentence also rules out commands.
You should receive a plain-language summary of the policy without an edit approval. You should also avoid seeing a command approval.
Did the agent request an unexpected action?
Choose reject if the agent requests an edit or command. Confirm that the prompt includes both final constraints exactly.
Ask for help reviewing the unexpected request: Help me understand why this read-only OpenCode prompt requested approval.
Your agent now has a visible safety boundary around a small workspace. Next, you'll let it inspect the starter project before approving one controlled baseline run.
Inspect and Run the Baseline
Your permission-gated sandbox is ready. OpenCode can read the SQL inventory workspace through its local model.
Before you trust the agent with edits, you need evidence that it understands the project. This baseline run creates that reference point without changing any files.
In this step, get ready to:
- Ask the agent to inspect the script without editing it.
- Approve one baseline script run.
- Compare the output with the files in the input folder.
Inspect the starter project
A code explanation gives you a low-risk way to check the agent's understanding. The read permission lets OpenCode inspect the workspace without requesting approval.
- Return to the OpenCode session in the integrated terminal from earlier.
- Paste this inspection request into OpenCode:
Inspect sql_inventory.py and the input folder. Explain what the script does, but do not edit anything.
What Does This Prompt Control?
- The inspection scope directs the agent to sql_inventory.py and the input folder.
- The final constraint keeps the project files unchanged during this review.
- Press Enter to send the request.
- Compare the explanation with the existing code in sql_inventory.py.
OpenCode should explain that find_sql_files() checks regular files in input for the .sql suffix. It should identify main() as the part that prints the results.
Does the Explanation Look Off?
- Check that the OpenCode session is still running from ~/local-agent-lab.
- Reject any edit request because this inspection only needs read access.
Help me compare OpenCode's explanation with my existing SQL inventory script.
Run the baseline through the approval gate
Running the script through OpenCode exercises the bash approval gate. The agent pauses before the command executes.
Before you send this, do you expect the agent to run immediately or pause for approval? Hold that prediction until you see the response.
- Paste this baseline request into OpenCode:
Run python3 sql_inventory.py. Ask for approval before running it, and report the exact output.
What Does This Prompt Control?
- The command asks Python to execute the existing inventory script.
- The approval constraint keeps shell execution under your control.
- The final instruction makes the observed output easy to compare with the input files.
- Press Enter to send the request.
- Review the proposed shell command.
- Select once to approve only this run.
You should see Found 1 SQL file(s): followed by - first.sql.
That is your first controlled agent run completed. The sandbox allowed one approved command without changing any project files.
Did the Baseline Run Fail?
- Reject the run if the proposed command differs from the request.
- Check that the active OpenCode session still points to ~/local-agent-lab if the script cannot see input.
- Send the original baseline request again if the agent did not ask for approval.
Help me diagnose why my approved OpenCode baseline run did not produce the expected file inventory.
Compare the output with the input folder
A baseline becomes useful when its output matches the files you can see. This comparison proves the script includes the SQL file while ignoring unrelated text.
- Expand the input folder in the VS Code Explorer sidebar.
- Select first.sql to confirm it is the SQL input.
- Select notes.txt to confirm it is unrelated text.
Before you compare the folder with the terminal, which filename should appear in the inventory? Make your prediction before checking the output again.
- Compare the terminal output with the two filenames in input.
You should see Found 1 SQL file(s): followed by - first.sql. You should not see notes.txt.
Baseline confirmed. Your local agent can understand the starter project and run a command through a visible approval gate.
The baseline is reliable. Next, you'll test how it behaves when the inputs change.
Expose the Case-Sensitivity Bug
Your approved baseline proved that OpenCode can run the inventory inside its guarded workspace. That clean result gives you a known starting point.
A clean baseline can still hide assumptions about filename spelling. An uppercase SQL extension changes one condition while the script stays untouched.
In this step, get ready to:
- Create input/SECOND.SQL through an approval-gated edit.
- Run the unchanged inventory against both SQL files.
- Have the agent explain the observed result without editing files.
Create the Uppercase SQL File
A controlled test changes one input condition at a time. The new file keeps the query simple while changing the extension from lowercase to uppercase.
- Switch back to the OpenCode session connected to your local model.
- Ask the agent to create only the new input file by sending this prompt:
Create input/SECOND.SQL containing SELECT 2; and show the file edit for approval. Do not change sql_inventory.py.
What Does This Prompt Control?
- The named path limits the requested change to input/SECOND.SQL.
- The final sentence protects sql_inventory.py from an early repair.
- The approval request gives you a chance to inspect the exact edit before it reaches the workspace.
The approval prompt is your chance to protect the working baseline. A one-time approval limits permission to this file creation.
- Review the proposed file edit.
- Confirm the diff creates only input/SECOND.SQL.
- Choose once to approve the file creation.
- Expand the input folder in the Visual Studio Code Explorer sidebar.
- Select SECOND.SQL to verify its contents.
You should see SECOND.SQL under input. The editor should show the one-line query you approved.
Agent Proposing Extra Changes?
- Choose reject if the proposed diff touches sql_inventory.py.
- Send the original prompt again after rejecting the broader edit.
- Confirm the requested path is exactly input/SECOND.SQL if the new file appears elsewhere.
Help me check why OpenCode is proposing files outside the requested path.
Run the Unchanged Inventory
A controlled run keeps the program unchanged while the input set grows. The result shows whether the inventory treats both extension styles alike.
- Ask OpenCode to propose the same inventory command by sending this prompt:
Run python3 sql_inventory.py and ask before executing it.
What Does This Prompt Test?
- OpenCode must pass the shell command through the existing bash approval gate.
- The unchanged command tests the same script against the expanded input folder.
- A one-time approval keeps future shell commands behind the gate.
Before you approve the run, do you think the reported count will match the two SQL files in input?
- Review the proposed shell command.
- Confirm the command is python3 sql_inventory.py.
- Choose once to approve this run.
You'll see Found 1 SQL file(s): followed by - first.sql.
The folder contains two SQL files. The inventory still reports one.
You now have a reproducible failure. The unchanged script gives the agent a clean target for diagnosis.
Output Does Not Show the Intended Gap?
- Check that input/SECOND.SQL appears in the Visual Studio Code Explorer.
- Check that sql_inventory.py still contains path.suffix == ".sql".
- Check that the uppercase filename is exactly SECOND.SQL.
Help me determine why my inventory output differs from the intended one-file result.
Ask the Agent to Diagnose the Gap
A useful diagnosis connects the visible failure to a specific comparison in the code. Keeping edits blocked separates understanding from repair.
Before you ask for a diagnosis, which filename detail do you expect OpenCode to focus on?
- Ask the agent to investigate without changing the workspace by sending this prompt:
Diagnose why SECOND.SQL is missing. Do not edit files yet.
Why Separate Diagnosis From Repair?
- The agent can inspect the current comparison before proposing a solution.
- The edit gate remains unused while you check whether the explanation fits the evidence.
- This pause keeps you in control of the next code change.
- Compare the agent's explanation with path.suffix == ".sql" in sql_inventory.py.
- Confirm the explanation identifies the equality comparison as case-sensitive.
- Reject any edit request before continuing.
The agent should explain that SECOND.SQL has the suffix .SQL. The current comparison accepts only the exact lowercase string .sql.
That diagnosis accounts for the missing file. No repair has been made yet.
- Use the comparison tabs below to confirm the new file exists while every earlier artifact stays unchanged.
✔️ Awesome, I've got everything!
The uppercase SQL file is present. The inventory script remains unchanged for the next step.
ⓧ I'd like to double check the full code
- Compare each workspace file with the references below.
SELECT 2;
This is the only new file created in this step.
SELECT 1;
This lowercase SQL fixture remains unchanged from the baseline.
This file should not appear in the SQL inventory.
This unrelated text file remains available for the inventory to ignore.
{
"$schema": "https://opencode.ai/config.json",
"share": "disabled",
"permission": {
"read": "allow",
"edit": "ask",
"bash": "ask",
"external_directory": "deny",
"webfetch": "deny",
"websearch": "deny"
}
}
The OpenCode approval gates remain unchanged.
from pathlib import Path
def find_sql_files(folder):
return sorted(
(
path
for path in folder.iterdir()
if path.is_file() and path.suffix == ".sql"
),
key=lambda path: path.name.lower(),
)
def main():
input_folder = Path("input")
if not input_folder.exists():
print("Input folder not found: input")
return
sql_files = find_sql_files(input_folder)
if not sql_files:
print("No .sql files found.")
return
print(f"Found {len(sql_files)} SQL file(s):")
for path in sql_files:
print(f"- {path.name}")
if __name__ == "__main__":
main()
The inventory still uses the intentionally case-sensitive suffix comparison.
You reproduced the hidden filename bug while keeping the sandbox controls intact. Next, you'll review a focused repair.
Fix and Test the SQL Inventory
The missing uppercase file gave you a reproducible failure. OpenCode has already traced it to a case-sensitive suffix comparison.
A diagnosis alone cannot protect the SQL inventory from the same bug returning. You will constrain the repair before using repeatable unit tests to prove it works.
In this step, get ready to:
- Constrain OpenCode to the diagnosed repair.
- Review each proposed file edit before granting approval.
- Verify the repair with tests plus the real input folder.
Constrain the repair
A constrained prompt defines the only acceptable code change. It also names the evidence the agent must produce.
- Switch back to the OpenCode session in the integrated terminal.
- Send the focused repair request by entering this exact prompt:
Change only find_sql_files so the suffix comparison is case-insensitive by using path.suffix.lower() == ".sql". Add test_sql_inventory.py with one test for lowercase and uppercase SQL names and one test that ignores notes.txt. Show each edit for approval. Then ask before running python3 -m unittest -v and python3 sql_inventory.py.
How does this prompt control the agent?
The prompt limits the repair to find_sql_files(). It requires separate evidence from the test suite plus the real input folder.
OpenCode should now present its first proposed file change for your review.
Agent proposing extra changes?
Reject any edit that changes unrelated behavior. Resend the original prompt without expanding its scope.
Help me keep this OpenCode repair limited to the requested function plus tests.
Review the proposed repair
An approval gate lets you inspect each change before it reaches the workspace. A focused diff keeps the existing file filtering plus sorting behavior intact.
Approving an edit changes only the listed local file. Your shell-command gate remains active for the later checks.
- Inspect the proposed diff for sql_inventory.py.
- Confirm the comparison changes to path.suffix.lower() == ".sql".
- Confirm the path.is_file() filter remains.
- Confirm the path.name.lower() sort key remains.
What should the repair preserve?
- The lowercase suffix comparison allows .sql plus .SQL files through the same filter.
- The file check continues to exclude folders.
- The lowercase name key keeps the results sorted without changing the displayed filenames.
- Select once to approve the focused sql_inventory.py edit.
- Inspect the proposed diff for test_sql_inventory.py.
- Confirm both tests use TemporaryDirectory for isolated input files.
- Confirm the first test writes first.sql plus SECOND.SQL.
- Confirm the second test writes query.sql plus notes.txt.
- Select once to approve the focused test_sql_inventory.py edit.
- Confirm test_sql_inventory.py appears in Visual Studio Code's left sidebar file list.
Your workspace now contains the repaired function plus a two-test safety net.
Diff does not match the request?
Choose reject if either diff changes unrelated code. Ask OpenCode to retry with only the missing focused edit.
Help me compare this proposed agent diff with the required focused repair.
Verify the repair twice
Each test creates an isolated temporary folder with controlled filenames. This proves the fix without depending on the current contents of input.
Before you approve the first command, do you expect both tests to pass after the focused edit?
- Compare OpenCode's proposed test command with this exact command:
python3 -m unittest -v
What does this test command prove?
Python discovers the test module with verbose reporting. The final status summarizes whether every discovered test passed.
- Select once to approve the test command.
- Wait for the test run to finish.
You will see two tests run. The final status is OK.
Good work. Your repair now has repeatable proof against both matching and ignored filenames.
Tests do not end with OK?
Compare the test method names with the reviewed diff. Check that both temporary folders create the filenames required by their assertions.
Help me diagnose why these SQL inventory unit tests are failing.
Before you approve the inventory command, which two filenames should survive the repaired filter?
- Compare OpenCode's proposed inventory command with this exact command:
python3 sql_inventory.py
What does this inventory command prove?
This runs the repaired inventory against the real input folder. It confirms the isolated tests match the workspace behavior.
- Select once to approve the inventory command.
You will see Found 2 SQL file(s): followed by first.sql plus SECOND.SQL. The output omits notes.txt.
You have turned the agent's diagnosis into a reviewed repair with visible test evidence.
Inventory still misses SECOND.SQL?
Check that the saved comparison contains path.suffix.lower(). Confirm SECOND.SQL remains inside the input folder.
Help me find why the repaired inventory still misses the uppercase SQL file.
✔️ Awesome, I've got everything!
Your focused repair is saved. Both tests pass before the real inventory lists both SQL files.
ⓧ I'd like to double check the full code
Compare your project configuration with this complete opencode.json file.
{
"$schema": "https://opencode.ai/config.json",
"share": "disabled",
"permission": {
"read": "allow",
"edit": "ask",
"bash": "ask",
"external_directory": "deny",
"webfetch": "deny",
"websearch": "deny"
}
}
What does this configuration preserve?
This configuration keeps sharing disabled. It requires approval for edits plus shell commands while denying external-directory and web access.
Compare your repaired module with this complete sql_inventory.py file.
from pathlib import Path
def find_sql_files(folder):
return sorted(
(
path
for path in folder.iterdir()
if path.is_file() and path.suffix.lower() == ".sql"
),
key=lambda path: path.name.lower(),
)
def main():
input_folder = Path("input")
if not input_folder.exists():
print("Input folder not found: input")
return
sql_files = find_sql_files(input_folder)
if not sql_files:
print("No .sql files found.")
return
print(f"Found {len(sql_files)} SQL file(s):")
for path in sql_files:
print(f"- {path.name}")
if __name__ == "__main__":
main()
What changed in the inventory?
The suffix is converted to lowercase before comparison. The existing file filter plus case-insensitive name sorting remain intact.
Compare your test module with this complete test_sql_inventory.py file.
import unittest
from pathlib import Path
from tempfile import TemporaryDirectory
from sql_inventory import find_sql_files
class FindSqlFilesTests(unittest.TestCase):
def test_matches_sql_suffix_case_insensitively(self):
with TemporaryDirectory() as directory:
folder = Path(directory)
(folder / "first.sql").write_text("SELECT 1;\n", encoding="utf-8")
(folder / "SECOND.SQL").write_text("SELECT 2;\n", encoding="utf-8")
names = [path.name for path in find_sql_files(folder)]
self.assertEqual(names, ["first.sql", "SECOND.SQL"])
def test_ignores_non_sql_files(self):
with TemporaryDirectory() as directory:
folder = Path(directory)
(folder / "query.sql").write_text("SELECT 1;\n", encoding="utf-8")
(folder / "notes.txt").write_text("not SQL\n", encoding="utf-8")
names = [path.name for path in find_sql_files(folder)]
self.assertEqual(names, ["query.sql"])
if __name__ == "__main__":
unittest.main()
What do these tests cover?
One test proves mixed-case SQL suffixes match. The other proves unrelated text files remain excluded.
Compare your first sample file with this complete input/first.sql file.
SELECT 1;
What does this sample represent?
This lowercase-extension file verifies the original matching behavior remains available.
Compare your uppercase sample file with this complete input/SECOND.SQL file.
SELECT 2;
What does this sample expose?
This uppercase-extension file reproduces the case that the original comparison missed.
Compare your ignored sample file with this complete input/notes.txt file.
This file should not appear in the SQL inventory.
What does this sample confirm?
This text file confirms the inventory still excludes unrelated files.
Secret mission
Undo a Bad Agent Edit
Approve a harmless wording change and watch it alter your script's visible result. Then use `/undo` to restore the tested version and prove the recovery with the same script and tests.
Clean Up Your Resources
Clean Up Your Resources
Everything in this project ran locally, so no paid cloud resource or API key can keep charging you. Choose whether to keep the setup, pause the model, or delete the local resources.
Resources you used:
- Ollama version 0.40.2 installed in the Applications folder.
- OpenCode version 1.18.35 available in Terminal.
- The qwen3:4b-thinking model using 2.5 GB of disk space.
- The ~/local-agent-lab workspace in Visual Studio Code.
Keep everything running
Keep this setup if you want to practise more local-agent workflows. No cleanup action is needed.
- Leave Ollama installed with the qwen3:4b-thinking model ready for local inference.
- Leave OpenCode installed for future permission-gated coding sessions.
- Keep ~/local-agent-lab as a tested example of safe agent-assisted debugging.
Pause - I'll come back to this later
Pausing unloads the running model while keeping its 2.5 GB download on disk. Your tools and workspace remain ready for later.
- Stop the running model by running this command in Terminal:
ollama stop qwen3:4b-thinking
What Does This Command Do?
This stops the running model. The downloaded model stays on your Mac for your next OpenCode session.
Model Won't Stop?
Confirm Ollama is still available in Terminal. A model that has already stopped has no running process left to unload.
Help me stop the local Ollama model.
Delete - I don't want to use this again
Deleting this setup removes its model, tools, and project files from your Mac. You can reinstall each part later.
- Remove the local model by running this command in Terminal:
ollama rm qwen3:4b-thinking
What Does This Command Do?
This removes the downloaded model from Ollama. It frees the disk space used by the model files.
Model Won't Delete?
Check that the model name is exactly qwen3:4b-thinking. A different model name points Ollama at a different local download.
Help me remove the local model.
- Uninstall OpenCode by running this command in Terminal:
opencode uninstall
What Does This Command Do?
This removes OpenCode and its related files from your Mac.
OpenCode Won't Uninstall?
Confirm the OpenCode executable is still available in Terminal. The uninstall command depends on that executable.
Help me uninstall OpenCode.
- In Finder, select the Applications folder.
- Move Ollama to Trash.
- Confirm Ollama no longer appears in Applications.
- In Finder, return to your home folder.
- Move ~/local-agent-lab to Trash.
- Confirm local-agent-lab no longer appears in your home folder.
Nice Work!
Nice Work!
You did it! Your OpenCode agent now runs through a local Ollama model inside a permission-gated workspace. You completed a debugging loop from visible failure to a tested Python fix.
You've learned how to:
- Run a terminal coding agent through local inference with qwen3:4b-thinking, so no cloud API key enters the workflow.
- Enforce approval gates for agent edits and shell commands while blocking external-directory and web access.
- Turn a visible case-sensitivity failure into a reviewed Python fix backed by two repeatable unit tests.
- Secret Mission: Practise controlled rollback after an approved output regression by using OpenCode's /undo workflow.
Ready to quiz yourself?