A multi-turn conversational AI agent CLI built in Go, featuring dual agent roles (planner + executor), pluggable skills, and a rich terminal UI.
- Multi-turn conversations - Interactive chat with context maintained across turns
- Dual agent architecture - Planner decomposes tasks, Executor runs them with tools
- Pluggable tools - Built-in: shell execution, file read/write, code execution (Python, JavaScript, Go, Rust, etc.)
- Agent Skills - Built-in domain expertise (git-commit, code-review, debugging, refactoring) + custom skills support
- Dual LLM backend - OpenAI-compatible API and Anthropic API
- Session memory - Remember instructions like "be concise" across turns
- Session persistence - Save and resume chat sessions across restarts
- TOML config file - Configuration stored in
~/.harness-cli/config.toml - Rich TUI - Bubble Tea powered terminal interface with live progress updates
- Go 1.21+
- An API key for OpenAI or Anthropic
git clone https://github.com/lispking/harness-cli.git
cd harness-cli
go build -o harness-cli .On first run, a config file is created at ~/.harness-cli/config.toml:
# LLM provider: "openai" or "anthropic"
provider = "openai"
# API key for the selected provider.
# You can also set OPENAI_API_KEY or ANTHROPIC_API_KEY environment variable instead.
api_key = ""
# API base URL. Leave empty to use the provider's default endpoint.
# Examples:
# OpenAI: https://api.openai.com/v1
# Anthropic: https://api.anthropic.com
# Ollama: http://localhost:11434/v1
# DeepSeek: https://api.deepseek.com/v1
base_url = ""
# Default model name. Can be overridden with --model flag.
model = "gpt-5.4"Alternatively, you can set API keys via environment variables:
export OPENAI_API_KEY="sk-..."
# or
export ANTHROPIC_API_KEY="sk-ant-..."# Launch the interactive TUI (uses config file settings)
./harness-cli
# Override the model
./harness-cli -m claude-sonnet-4-6
# Resume the most recent session
./harness-cli -c
# Resume a specific session by ID
./harness-cli -s 2026-04-12_14-30-00Inside the chat session:
| Command | Description |
|---|---|
/help |
Show available commands |
/quit or /exit |
Exit the chat (auto-saves session) |
/clear |
Clear conversation history |
/remember <text> |
Remember an instruction for this session |
/forget |
Clear all remembered instructions |
/memory |
Show current remembered instructions |
/save |
Save current session to disk |
/sessions |
List all saved sessions |
/skills |
Open skill picker to activate a skill |
> list the Go files in the current directory
⣾ Planning...
[Calling tool: shell_exec]
[Executing tool: shell_exec]
[Result]: main.go
...
> /remember be concise
Remembered: be concise
> what time is it
[Calling tool: shell_exec]
[Executing tool: shell_exec]
[Result]: Sat Apr 12 14:32:01 CST 2026
Sat Apr 12 14:32:01 CST 2026
(3.2s | in:45 out:120)
> write a python script to calculate fibonacci
[Calling tool: write_file]
[Executing tool: write_file]
[Result]: File written successfully
[Calling tool: code_exec]
[Executing tool: code_exec]
[Result]: [1, 1, 2, 3, 5, 8, 13, 21, 34, 55]
I've created a Python script to calculate the first 10 Fibonacci numbers:
[1, 1, 2, 3, 5, 8, 13, 21, 34, 55]
(5.8s | in:156 out:340)
User Input
|
v
+----------+ +------------------+
| Planner |---->| Plan (JSON) |
| Agent | | {steps:[...]} |
+----------+ +--------+---------+
|
v
+------------------+
| Executor Agent |
| (tool-call loop)|
| |
| +-------------+ |
| | Tool Reg. | |
| | - shell_exec| |
| | - read_file | |
| | - write_file| |
| | - code_exec | |
| +-------------+ |
+--------+---------+
|
v
Final Response
+-- main.go # Entry point
+-- cmd/
| +-- root.go # Root command + TUI launch
| +-- version.go # Version command
+-- config/
| +-- config.go # TOML config file loading
+-- internal/
| +-- llm/
| | +-- types.go # Message, ToolCall, Response
| | +-- client.go # Client interface
| | +-- openai.go # OpenAI implementation
| | +-- anthropic.go # Anthropic implementation
| | +-- retry.go # Retry with exponential backoff
| +-- agent/
| | +-- agent.go # Agent interface
| | +-- planner.go # Task decomposition
| | +-- executor.go # Step execution with tools
| +-- tool/
| | +-- tool.go # Tool interface + Registry
| | +-- shell.go # Shell command execution
| | +-- read.go # File reading
| | +-- write.go # File writing
| | +-- code.go # Code execution (Python, JS, Go, Rust, etc.)
| +-- skill/
| | +-- skill.go # Agent Skills support
| | +-- builtin/ # Built-in skills
| | +-- git-commit/
| | +-- code-review/
| | +-- debugging/
| | +-- refactoring/
| +-- memory/
| | +-- memory.go # Session-scoped memory
| +-- session/
| | +-- session.go # Session persistence to disk
| +-- tui/
| +-- model.go # Bubble Tea model
| +-- view.go # Rendering
| +-- update.go # Message handling
Harness CLI includes built-in skills for common tasks and supports custom skills via agentskills.io specification.
| Skill | Description | Activated When |
|---|---|---|
git-commit |
Conventional Commits style messages | "commit this", "write commit message" |
code-review |
Code quality & security review | "review this code", "check this" |
debugging |
Systematic debugging guidance | "it's not working", "fix this bug" |
refactoring |
Safe refactoring techniques | "clean up this code", "refactor" |
Skills activate automatically when relevant to your task.
Manual Activation:
Type /skills to open the skill picker and manually activate a skill. Use ↑↓ to navigate, Enter to activate, Esc to cancel.
Create your own skills in ~/.harness-cli/skills/:
skills/
└── my-skill/
├── SKILL.md # Required: metadata + instructions
├── scripts/ # Optional: executable code
├── references/ # Optional: documentation
└── assets/ # Optional: templates
SKILL.md format:
---
name: my-skill
description: What this does. Use when the user needs X.
---
# My Skill
## When to Use
Explain when to activate this skill.
## Instructions
Step-by-step guidance for the agent.Skills are automatically discovered and activated when relevant.
Implement the Tool interface and register it:
// internal/tool/my_tool.go
package tool
import "context"
type MyTool struct{}
func (t *MyTool) Name() string { return "my_tool" }
func (t *MyTool) Description() string { return "Does something custom" }
func (t *MyTool) Parameters() map[string]any {
return schema(map[string]any{
"input": map[string]any{"type": "string"},
}, []string{"input"})
}
func (t *MyTool) Execute(ctx context.Context, argsJSON string) (string, error) {
// Your logic here
return "result", nil
}Then register in cmd/root.go:
registry.Register(tool.NewMyTool())MIT