The Complete OpenCode Setup: From Shell Scripts to AI Workflows

Part 4 of Customizing OpenCode, bringing together Custom Tools, Shell Scripts, and Agents & Commands.

I've written about each piece of OpenCode's customization system separately. This post ties them all together with one example you can actually build: a code quality workflow.

The Stack

Here's how the pieces connect:

  1. Shell scripts — Human-runnable, tested, version-controlled
  2. Custom tools — Wrap scripts so the LLM can call them with validated args
  3. Agents/Subagents — Control which tools are available and how the AI approaches tasks
  4. Commands — Shortcuts that trigger specific agents with specific prompts

Each layer builds on the one below. Let's build all four for a single workflow: running tests, checking coverage, and fixing failures.

The Example: Test & Fix Workflow

Every project has tests. Every developer runs them, checks coverage, and fixes failures. We'll automate this entire flow.

By the end, you'll have:

  • A script you can run manually: ./scripts/test.sh
  • A tool the LLM can call with validated arguments
  • A subagent specialized in analyzing test failures
  • A command that triggers the whole workflow with /test

Layer 1: The Shell Script

Start with something that works on its own.

#!/bin/bash
# .opencode/scripts/test.sh

set -e

# Default values
COVERAGE=${1:-false}
PATH_FILTER=${2:-.}

echo "🧪 Running tests..."

if [ "$COVERAGE" = "true" ]; then
    npm test -- --coverage --coverageReporters=text --passWithNoTests $PATH_FILTER
else
    npm test -- --passWithNoTests $PATH_FILTER
fi

echo "✅ Tests complete"

Make it executable and test it:

chmod +x .opencode/scripts/test.sh

# Run all tests
./.opencode/scripts/test.sh

# Run with coverage
./.opencode/scripts/test.sh true

# Run tests in a specific directory
./.opencode/scripts/test.sh false src/utils

This script works. Anyone on your team can run it. It's in version control. Now we wrap it.


Layer 2: The Custom Tool

The tool definition validates arguments and calls the script.

// .opencode/tool/test.ts
import { tool } from "@opencode-ai/plugin"

export default tool({
  description: "Run the project test suite with optional coverage reporting",
  args: {
    coverage: tool.schema
      .boolean()
      .optional()
      .describe("Generate coverage report (default: false)"),
    path: tool.schema
      .string()
      .optional()
      .describe("Filter tests by path (default: all tests)"),
  },
  async execute(args) {
    const coverage = args.coverage ? "true" : "false"
    const path = args.path || "."
    return await Bun.$`./.opencode/scripts/test.sh ${coverage} ${path}`.text()
  },
})

Now the LLM can run tests. But it can only pass valid arguments—boolean for coverage, string for path. No garbage inputs hitting your script.

You can tell OpenCode:

  • "Run the tests"
  • "Run tests with coverage"
  • "Run tests for src/utils"

It calls your script with the right arguments.


Layer 3: The Subagent

A generic Build agent can run tests, but it doesn't specialize in analyzing failures. Let's create a subagent that does.

# .opencode/agent/test-analyzer.md
---
description: Analyzes test failures and suggests fixes
mode: subagent
model: anthropic/claude-sonnet-4-20250514
tools:
  test: true
  read: true
  grep: true
  glob: true
  write: false
  bash: false
---

You are a test analysis specialist. When invoked:

1. Run the tests using the test tool to see current state
2. For any failures:
   - Read the failing test file
   - Read the source file being tested
   - Identify why the test is failing
   - Provide a specific fix

Focus on:
- Understanding what the test expects vs what it gets
- Checking if the test is wrong or the implementation is wrong
- Suggesting minimal changes to fix the issue

Be concise. Show the exact code changes needed.

Key decisions here:

  • mode: subagent — Runs in isolation, doesn't pollute your main context
  • test: true — Can use our custom test tool
  • write: false — Can analyze but not modify (you review first)
  • bash: false — Can only run tests through the controlled tool

The subagent can run tests and read files, but can't make arbitrary changes. You stay in control.


Layer 4: The Command

Now wire it up with a command that's easy to trigger.

# .opencode/command/test.md
---
description: Run tests and analyze any failures
agent: test-analyzer
subtask: true
---

Run the test suite with coverage.

If there are failures:
- Analyze each failing test
- Explain why it's failing  
- Suggest specific fixes

If all tests pass:
- Summarize the coverage report
- Note any files with low coverage

Usage:

/test

That's it. One command triggers the subagent, which uses the custom tool, which runs your shell script. The full chain executes, and you get a focused analysis without cluttering your main conversation.

Variations

You can create multiple commands for different scenarios:

# .opencode/command/test-file.md
---
description: Run tests for a specific file
agent: test-analyzer
subtask: true
---

Run tests for: $ARGUMENTS

Analyze any failures and suggest fixes.

Usage: /test-file src/utils/parser.test.ts

# .opencode/command/coverage.md
---
description: Analyze test coverage
agent: test-analyzer
subtask: true
---

Run the full test suite with coverage.

Focus on:
- Files with less than 80% coverage
- Untested functions or branches
- Suggest which tests to add

Don't worry about failures right now, just coverage gaps.

Usage: /coverage


The Complete Directory Structure

Here's everything in place:

.opencode/
├── scripts/
│   └── test.sh              # Human-runnable test script
├── tool/
│   └── test.ts              # Tool wrapper with validation
├── agent/
│   └── test-analyzer.md     # Specialized subagent
└── command/
    ├── test.md              # Run tests + analyze failures
    ├── test-file.md         # Test specific file
    └── coverage.md          # Coverage analysis

How It All Flows

Let's trace what happens when you type /test:

/test
  ↓
Command triggers test-analyzer subagent (isolated context)
  ↓
Subagent reads its system prompt (analyze failures, suggest fixes)
  ↓
Subagent calls the test tool with coverage: true
  ↓
Tool validates args and runs: ./.opencode/scripts/test.sh true .
  ↓
Script executes: npm test -- --coverage
  ↓
Output returns through the chain
  ↓
Subagent analyzes results, reads relevant files
  ↓
You get a focused report with specific fixes
  ↓
Main conversation context stays clean

Each layer does one job:

  • Script: runs the actual commands
  • Tool: validates inputs, calls script
  • Agent: shapes how the AI thinks about the task
  • Command: triggers the workflow

Extending the Pattern

Once you have this working, the pattern applies everywhere.

Linting Workflow

# .opencode/scripts/lint.sh
#!/bin/bash
set -e
FIX=${1:-false}

if [ "$FIX" = "true" ]; then
    npm run lint -- --fix
else
    npm run lint
fi
// .opencode/tool/lint.ts
export default tool({
  description: "Run linter on the codebase",
  args: {
    fix: tool.schema.boolean().optional().describe("Auto-fix issues"),
  },
  async execute(args) {
    const fix = args.fix ? "true" : "false"
    return await Bun.$`./.opencode/scripts/lint.sh ${fix}`.text()
  },
})
# .opencode/agent/lint-fixer.md
---
description: Fixes linting errors
mode: subagent
tools:
  lint: true
  read: true
  edit: true
---

You fix linting issues. Run the linter, then fix each error.
Prefer auto-fix when possible. Manual fixes for complex issues.

Git Workflow

# .opencode/scripts/git-status.sh
#!/bin/bash
echo "=== Branch ==="
git branch --show-current

echo -e "\n=== Status ==="
git status --short

echo -e "\n=== Recent Commits ==="
git log --oneline -5
// .opencode/tool/git-status.ts
export default tool({
  description: "Show current git status, branch, and recent commits",
  args: {},
  async execute() {
    return await Bun.$`./.opencode/scripts/git-status.sh`.text()
  },
})
# .opencode/command/status.md
---
description: Show git status and suggest next steps
---

!`./.opencode/scripts/git-status.sh`

Based on this status:
- What's ready to commit?
- What needs attention?
- Suggest next steps.

Tips for Building Your Own

Start with the script. Get it working manually before adding layers. Debug at the lowest level.

Keep tools thin. They should validate and call the script, nothing more. Logic lives in the script.

Match agent capabilities to their role. Read-only agents for analysis, write access only for agents that need to make changes.

Use subtask: true on commands. Keeps your main context clean. You can always run without it if you need the back-and-forth.

One workflow at a time. Build the full stack for one thing before moving to the next. It's easier to debug and you'll learn the pattern.


Wrapping Up

The four layers give you increasing levels of control:

Layer What it does Who/what uses it
Shell script Runs the actual commands Humans, tools
Custom tool Validates args, calls script LLM agents
Agent Shapes how AI approaches the task Commands, direct chat
Command Triggers workflows with one keystroke You

Start from the bottom, build up. Each layer is useful on its own, and together they give you a complete system for controlled AI automation.


This is the final part of Customizing OpenCode. Start with Part 1: Custom Tools if you haven't already.

Questions? Find me on Twitter or drop me an email.

← Back to blog
Bun + HTMX
Ready
© 2026 Efra