Building a go cli cobra claude opus 4.8 tool is a practical way to wrap model calls in a scriptable interface your team can pipe into CI or local workflows. This tutorial ships a minimal but production-shaped CLI that authenticates to Anthropic, sends a prompt to claude-opus-4-8, and prints the response, using Cobra for command structure and nothing but the standard library for HTTP.
Prerequisites
- Go 1.22 or newer installed and on
PATH - The
cobrapackage (go get github.com/spf13/cobra@latest) - An Anthropic API key exported as
ANTHROPIC_API_KEY - Basic comfort with
encoding/jsonandnet/http curlhandy for sanity-checking the endpoint outside Go
If you prefer a single gateway instead of per-vendor keys, n4n.ai exposes one OpenAI-compatible endpoint that fronts Claude Opus 4.8 among 240+ models and handles provider fallback automatically. The code below targets Anthropic directly; swapping the base URL and auth header is trivial.
Initialize the module
mkdir opuscli && cd opuscli
go mod init github.com/you/opuscli
go get github.com/spf13/cobra@latest
Keep the module path consistent with your repo. Cobra pulls in pflag and yaml optionally, but we won’t need YAML for the core flow.
Root command skeleton
Create main.go with a root command that only prints help for now.
package main
import (
"fmt"
"os"
"github.com/spf13/cobra"
)
var rootCmd = &cobra.Command{
Use: "opuscli",
Short: "CLI for querying Claude Opus 4.8",
Long: "A minimal Go CLI built with Cobra to call claude-opus-4-8 via Anthropic.",
}
func execute() {
if err := rootCmd.Execute(); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
func main() {
execute()
}
Build and run to confirm Cobra is wired:
go build -o opuscli .
./opuscli --help
Expected output includes the Usage: opuscli block and the Flags: --help line. If you see command not found, check your go bin path or run ./opuscli from the project root.
Add the ask command
The core of our go cli cobra claude opus 4.8 is the ask subcommand. It takes exactly one positional argument (the prompt) and sends it to the model.
var askCmd = &cobra.Command{
Use: "ask [prompt]",
Short: "Send a prompt to Claude Opus 4.8",
Args: cobra.ExactArgs(1),
RunE: runAsk,
}
func init() {
rootCmd.AddCommand(askCmd)
}
We separate RunE to return errors cleanly. Cobra maps a non-nil error to a non-zero exit code via our execute() wrapper.
Implement the API call
Anthropic’s Messages API expects a JSON body. Here is the exact shape we send:
{
"model": "claude-opus-4-8",
"max_tokens": 1024,
"messages": [
{"role": "user", "content": "Explain rate limiting in one sentence."}
]
}
The Go function that performs the POST uses only stdlib:
func runAsk(cmd *cobra.Command, args []string) error {
apiKey := os.Getenv("ANTHROPIC_API_KEY")
if apiKey == "" {
return fmt.Errorf("ANTHROPIC_API_KEY not set")
}
body := map[string]interface{}{
"model": "claude-opus-4-8",
"max_tokens": 1024,
"messages": []map[string]string{
{"role": "user", "content": args[0]},
},
}
buf, _ := json.Marshal(body)
req, err := http.NewRequest("POST", "https://api.anthropic.com/v1/messages", bytes.NewReader(buf))
if err != nil {
return err
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("x-api-key", apiKey)
req.Header.Set("anthropic-version", "2023-06-01")
resp, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
respBody, _ := io.ReadAll(resp.Body)
return fmt.Errorf("api error %d: %s", resp.StatusCode, respBody)
}
var out struct {
Content []struct {
Text string `json:"text"`
} `json:"content"`
}
if err := json.NewDecoder(resp.Body).Decode(&out); err != nil {
return err
}
for _, c := range out.Content {
fmt.Println(c.Text)
}
return nil
}
Note the anthropic-version header—Anthropic requires it on every request. The response content array holds generated text blocks; we print each.
Add the missing imports to the top of the file:
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"github.com/spf13/cobra"
)
Run the tool
Export your key and fire a prompt:
export ANTHROPIC_API_KEY=sk-ant-...
./opuscli ask "What is the difference between TCP and UDP in one line?"
Expected output is a single sentence from Claude Opus 4.8, for example:
TCP provides ordered, reliable delivery with connection setup, while UDP sends datagrams without guarantees for speed.
If the key is missing, you get ANTHROPIC_API_KEY not set on stderr and exit code 1. If you typed the model name wrong, Anthropic returns 404 with a JSON error; our code prints api error 404: ....
Persist configuration with flags
Hard-coding env reliance is fine, but a real go cli cobra claude opus 4.8 should accept a --key flag and maybe --max-tokens. Extend askCmd:
var (
flagKey string
flagMaxTokens int
)
func init() {
askCmd.Flags().StringVar(&flagKey, "key", "", "Anthropic API key (overrides env)")
askCmd.Flags().IntVar(&flagMaxTokens, "max-tokens", 1024, "Max tokens to generate")
rootCmd.AddCommand(askCmd)
}
Then modify runAsk to prefer the flag:
apiKey := flagKey
if apiKey == "" {
apiKey = os.Getenv("ANTHROPIC_API_KEY")
}
And use flagMaxTokens in the request body instead of the literal 1024. This takes five minutes and makes the CLI usable in scripts where env injection is annoying.
Full source listing
For reference, the complete main.go after the above edits:
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"github.com/spf13/cobra"
)
var rootCmd = &cobra.Command{
Use: "opuscli",
Short: "CLI for querying Claude Opus 4.8",
}
var askCmd = &cobra.Command{
Use: "ask [prompt]",
Short: "Send a prompt to Claude Opus 4.8",
Args: cobra.ExactArgs(1),
RunE: runAsk,
}
var (
flagKey string
flagMaxTokens int
)
func init() {
askCmd.Flags().StringVar(&flagKey, "key", "", "Anthropic API key (overrides env)")
askCmd.Flags().IntVar(&flagMaxTokens, "max-tokens", 1024, "Max tokens to generate")
rootCmd.AddCommand(askCmd)
}
func runAsk(cmd *cobra.Command, args []string) error {
apiKey := flagKey
if apiKey == "" {
apiKey = os.Getenv("ANTHROPIC_API_KEY")
}
if apiKey == "" {
return fmt.Errorf("ANTHROPIC_API_KEY not set")
}
body := map[string]interface{}{
"model": "claude-opus-4-8",
"max_tokens": flagMaxTokens,
"messages": []map[string]string{
{"role": "user", "content": args[0]},
},
}
buf, _ := json.Marshal(body)
req, err := http.NewRequest("POST", "https://api.anthropic.com/v1/messages", bytes.NewReader(buf))
if err != nil {
return err
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("x-api-key", apiKey)
req.Header.Set("anthropic-version", "2023-06-01")
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
respBody, _ := io.ReadAll(resp.Body)
return fmt.Errorf("api error %d: %s", resp.StatusCode, respBody)
}
var out struct {
Content []struct {
Text string `json:"text"`
} `json:"content"`
}
if err := json.NewDecoder(resp.Body).Decode(&out); err != nil {
return err
}
for _, c := range out.Content {
fmt.Println(c.Text)
}
return nil
}
func execute() {
if err := rootCmd.Execute(); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
func main() {
execute()
}
Testing the CLI with httptest
Before shipping, stub the API with net/http/httptest to avoid burning tokens in unit tests:
// in a ask_test.go file
func TestRunAsk(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
fmt.Fprint(w, `{"content":[{"text":"ok"}]}`)
}))
defer srv.Close()
// override the URL via a package-level var or flag in real code
}
This pattern keeps your go cli cobra claude opus 4.8 logic covered without network dependence.
Swapping to a gateway
If you route through a gateway, change the POST URL and auth scheme. For an OpenAI-compatible front, the request body uses "model": "claude-opus-4-8" and Bearer auth instead of x-api-key. The Cobra surface stays identical; only runAsk transports differ.
Why Cobra
Cobra gives you --help, subcommands, and flag binding with near-zero boilerplate. For a go cli cobra claude opus 4.8, it means you spend time on the API contract, not on arg parsing. The alternative—hand-rolling flag.FlagSet—is fine for one command but falls apart when you add ask, stream, models, etc.
Final notes
Set a client timeout before shipping:
client := &http.Client{Timeout: 30 * time.Second}
Otherwise a stalled connection hangs your pipeline. Build the binary, drop it in /usr/local/bin, and you have a reusable model call from any shell script. The pattern—Cobra for surface, stdlib for transport—scales to any model endpoint without dragging in heavy SDKs.