> For the complete documentation index, see [llms.txt](https://developer.harness.io/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://developer.harness.io/harness-platform/3.0/in-harness-3.0/agents/custom.md).

# Building Custom Agents

Harness agent plugins follow the [Drone plugin architecture](https://github.com/thisrohangupta/agents/tree/main), a Go-based pattern where each plugin is a Docker container that receives configuration via environment variables and executes autonomous workflows. This guide covers the full development lifecycle from project setup to registration in the Harness Agents catalog.

|                  |                      |
| ---------------- | -------------------- |
| **Language**     | Go                   |
| **Architecture** | Drone plugin pattern |
| **Runtime**      | Docker container     |
| **Integration**  | Harness API          |

### Plugin architecture <a href="#plugin-architecture" id="plugin-architecture"></a>

Each plugin is a Go binary that runs inside a Docker container. Configuration is passed via environment variables with a `PLUGIN_` prefix, and the plugin implements a `Plugin` struct with an `Exec()` method.

#### Core components <a href="#core-components" id="core-components"></a>

* **CLI Framework (`main.go`)**: Uses `urfave/cli` for command-line argument parsing. Defines flags that map to `PLUGIN_` prefixed environment variables.
* **Business Logic (`plugin.go`)**: Contains the `Plugin` struct with all configuration fields and the `Exec()` method that implements the agent's core workflow.
* **Agent Binaries (`bin/`)**: Pre-compiled AI agent binaries (e.g., `ai-code-agent`, `remediation-agent`) that the plugin orchestrates.
* **Docker Container**: Multi-stage Dockerfile that builds the Go binary and packages it with runtime dependencies.

```go
package main

import (
    "os"
    "os/exec"
    "github.com/pkg/errors"
    "github.com/sirupsen/logrus"
)

type Plugin struct {
    // Required fields
    WorkingDirectory string
    AnthropicAPIKey  string
    // Optional features
    DetailedLogging  bool
    // Harness API integration
    HarnessAPIKey      string
    HarnessAccountID   string
    HarnessOrgID       string
    HarnessProjectID   string
    HarnessPipelineID  string
    HarnessExecutionID string
    HarnessBaseURL     string
}

func (p *Plugin) Exec() error {
    // 1. Validate configuration
    if p.WorkingDirectory == "" {
        return errors.New("working directory is required")
    }
    if p.AnthropicAPIKey == "" {
        return errors.New("Anthropic API key is required")
    }

    // 2. Setup logging
    if p.DetailedLogging {
        logrus.SetLevel(logrus.DebugLevel)
    }

    // 3. Execute agent binary
    cmd := exec.Command("/root/bin/ai-code-agent",
        "--working-dir", p.WorkingDirectory,
    )
    cmd.Dir = p.WorkingDirectory
    cmd.Stdout = os.Stdout
    cmd.Stderr = os.Stderr
    cmd.Env = append(os.Environ(),
        "ANTHROPIC_API_KEY="+p.AnthropicAPIKey,
    )

    if err := cmd.Run(); err != nil {
        return errors.Wrap(err, "agent execution failed")
    }
    return nil
}
```

{% hint style="info" %}
**DRONE PLUGIN PATTERN**

The Drone plugin architecture is the standard pattern for all Harness CI plugins. If you've built Drone plugins before, the same patterns apply to agent plugins.
{% endhint %}

***

### Project structure <a href="#project-structure" id="project-structure"></a>

```bash
my-agent-plugin/
├── main.go              # CLI entry point with flag definitions
├── plugin.go            # Plugin struct and Exec() business logic
├── go.mod               # Go module definition
├── go.sum               # Dependency checksums
├── Dockerfile           # Multi-stage Docker build
├── Makefile             # Build automation targets
├── README.md            # Plugin documentation
└── bin/                 # Pre-compiled agent binaries
    └── ai-code-agent    # AI coding agent binary
```

| File         | Purpose                                                                                                                 |
| ------------ | ----------------------------------------------------------------------------------------------------------------------- |
| `main.go`    | CLI entry point. Defines flags, maps to `PLUGIN_` environment variables, constructs `Plugin` struct, and calls `Exec()` |
| `plugin.go`  | Core business logic. Contains `Plugin` struct with configuration fields and the `Exec()` method                         |
| `go.mod`     | Go module definition with required dependencies (`urfave/cli`, `logrus`, `godotenv`, `pkg/errors`)                      |
| `Dockerfile` | Multi-stage Docker build: compile Go binary, then copy to slim runtime image with agent binaries                        |
| `Makefile`   | Build targets: `build` (local binary), `build-docker` (Docker image), `push` (Docker Hub)                               |
| `bin/`       | Directory for pre-compiled agent binaries that the plugin orchestrates at runtime                                       |

```go
package main

import (
    "os"
    "github.com/joho/godotenv"
    "github.com/sirupsen/logrus"
    "github.com/urfave/cli"
)

func main() {
    app := cli.NewApp()
    app.Name = "my-agent-plugin"
    app.Usage = "Harness AI Agent Plugin"
    app.Action = run
    app.Flags = []cli.Flag{
        cli.StringFlag{
            Name:   "working-directory",
            Usage:  "path to the git repository",
            EnvVar: "PLUGIN_WORKING_DIRECTORY",
        },
        cli.StringFlag{
            Name:   "anthropic-api-key",
            Usage:  "Anthropic API key for Claude",
            EnvVar: "PLUGIN_ANTHROPIC_API_KEY,ANTHROPIC_API_KEY",
        },
        cli.BoolFlag{
            Name:   "detailed-logging",
            Usage:  "enable debug-level logging",
            EnvVar: "PLUGIN_DETAILED_LOGGING",
        },
        cli.StringFlag{
            Name:   "prompt",
            Usage:  "task prompt for the agent",
            EnvVar: "PLUGIN_PROMPT",
        },
    }
    if err := app.Run(os.Args); err != nil {
        logrus.Fatal(err)
    }
}

func run(c *cli.Context) error {
    if env := c.String("env-file"); env != "" {
        godotenv.Load(env)
    }
    plugin := Plugin{
        WorkingDirectory: c.String("working-directory"),
        AnthropicAPIKey:  c.String("anthropic-api-key"),
        DetailedLogging:  c.Bool("detailed-logging"),
    }
    return plugin.Exec()
}
```

***

### Build the plugin <a href="#build-the-plugin" id="build-the-plugin"></a>

#### Required dependencies <a href="#required-dependencies" id="required-dependencies"></a>

```go
module my-agent-plugin

go 1.24

require (
    github.com/joho/godotenv v1.5.1
    github.com/pkg/errors v0.9.1
    github.com/sirupsen/logrus v1.9.3
    github.com/urfave/cli v1.22.14
)
```

#### Map environment variables <a href="#map-environment-variables" id="map-environment-variables"></a>

Each CLI flag maps to a `PLUGIN_` prefixed environment variable. Harness also auto-populates platform context variables at runtime.

| CLI Flag              | Environment Variable       | Description                       |
| --------------------- | -------------------------- | --------------------------------- |
| `--working-directory` | `PLUGIN_WORKING_DIRECTORY` | Path to the cloned git repository |
| `--anthropic-api-key` | `PLUGIN_ANTHROPIC_API_KEY` | Anthropic API key for Claude AI   |
| `--detailed-logging`  | `PLUGIN_DETAILED_LOGGING`  | Enable debug-level logging        |
| `--prompt`            | `PLUGIN_PROMPT`            | Task prompt for the agent         |
| (auto-populated)      | `HARNESS_ACCOUNT_ID`       | Harness account identifier        |
| (auto-populated)      | `HARNESS_ORG_ID`           | Harness organization identifier   |
| (auto-populated)      | `HARNESS_PROJECT_ID`       | Harness project identifier        |
| (auto-populated)      | `HARNESS_EXECUTION_ID`     | Pipeline execution identifier     |

#### Build with Makefile <a href="#build-with-makefile" id="build-with-makefile"></a>

```makefile
PLUGIN_NAME := my-agent-plugin
DOCKER_REPO := yourdockerhub/$(PLUGIN_NAME)

.PHONY: build build-docker push clean

build:
	CGO_ENABLED=0 GOOS=linux GOARCH=arm64 \
		go build -o $(PLUGIN_NAME) .

build-docker:
	docker build -t $(DOCKER_REPO):latest .

push:
	docker push $(DOCKER_REPO):latest

clean:
	rm -f $(PLUGIN_NAME)
```

{% hint style="info" %}
**AUTO-POPULATED VARIABLES**

Harness auto-populates `HARNESS_ACCOUNT_ID`, `HARNESS_ORG_ID`, `HARNESS_PROJECT_ID`, and `HARNESS_EXECUTION_ID` when running plugins in CI pipelines. You don't need to configure these manually.
{% endhint %}

***

### Docker package <a href="#docker-package" id="docker-package"></a>

Agent plugins use a multi-stage Docker build. The first stage compiles the Go binary; the second creates a minimal runtime image.

```dockerfile
# Stage 1: Build the Go plugin binary <a href="#stage-1-build-the-go-plugin-binary" id="stage-1-build-the-go-plugin-binary"></a>
FROM golang:1.24 AS builder

WORKDIR /app
COPY go.mod go.sum* ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux GOARCH=arm64 \
    go build -o my-agent-plugin .

# Stage 2: Create minimal runtime image <a href="#stage-2-create-minimal-runtime-image" id="stage-2-create-minimal-runtime-image"></a>
FROM --platform=linux/arm64 debian:bookworm-slim

RUN apt-get update && \
    apt-get install -y --no-install-recommends \
    ca-certificates git bash && \
    rm -rf /var/lib/apt/lists/*

WORKDIR /root/

COPY --from=builder /app/my-agent-plugin .
COPY bin/* ./bin/

RUN chmod +x ./my-agent-plugin ./bin/*

ENTRYPOINT ["/root/my-agent-plugin"]
```

Runtime dependencies: `ca-certificates` for HTTPS API calls, `git` for repository operations, `bash` for shell script execution. Agent binaries (`ai-code-agent`, `remediation-agent`) are \~24–27 MB each. Use `debian:bookworm-slim` as base and `CGO_ENABLED=0` for a statically-linked Go binary. Most plugins target `linux/arm64`.

{% hint style="warning" %}
**MULTI-STAGE BUILDS**

Always use multi-stage Docker builds. Never include Go toolchain, source code, or build artifacts in the runtime image.
{% endhint %}

***

### Harness API integration <a href="#harness-api-integration" id="harness-api-integration"></a>

Plugins can integrate with the Harness API to fetch pipeline execution data and retrieve logs from failed steps.

```bash
# Fetch pipeline execution details (includes step graph) <a href="#fetch-pipeline-execution-details-includes-step-graph" id="fetch-pipeline-execution-details-includes-step-graph"></a>
GET /pipeline/api/pipelines/execution/{executionId}
    ?accountIdentifier={accountId}
    &orgIdentifier={orgId}
    &projectIdentifier={projectId}

# Download step logs from the log service <a href="#download-step-logs-from-the-log-service" id="download-step-logs-from-the-log-service"></a>
POST /log-service/blob/download
    ?accountID={accountId}
    &prefix={logBaseKey}
```

To find and diagnose failed steps, traverse the execution graph, look for steps with status `Failed` or `IgnoreFailed`, validate presence of `failureInfo` with error messages, and extract the `logBaseKey` field for log retrieval.

```go
func (p *Plugin) fetchFailedStepLogs() (string, error) {
    url := fmt.Sprintf(
        "%s/pipeline/api/pipelines/execution/%s"+
        "?accountIdentifier=%s&orgIdentifier=%s&projectIdentifier=%s",
        p.HarnessBaseURL,
        p.HarnessExecutionID,
        p.HarnessAccountID,
        p.HarnessOrgID,
        p.HarnessProjectID,
    )

    req, _ := http.NewRequest("GET", url, nil)
    req.Header.Set("x-api-key", p.HarnessAPIKey)

    resp, err := http.DefaultClient.Do(req)
    if err != nil {
        return "", errors.Wrap(err, "failed to fetch execution")
    }
    defer resp.Body.Close()

    // Parse response, find failed steps, extract logBaseKey, download logs...
    return logs, nil
}
```

The Harness API key is passed as the `x-api-key` header. Ensure your API key has permissions to read pipeline executions and logs for the target project.

***

### Template registration <a href="#template-registration" id="template-registration"></a>

Once your plugin image is pushed, register it as a Harness Agent by creating a template in the [agents repository](https://github.com/thisrohangupta/agents/tree/main).

#### Step 1: Create a template directory <a href="#step-1-create-a-template-directory" id="step-1-create-a-template-directory"></a>

```bash
templates/my-custom-agent/
├── metadata.json    # Required: name, description, version
├── pipeline.yaml    # Required: pipeline definition using your plugin
├── wiki.MD          # Optional: user-facing documentation
└── logo.svg         # Optional: icon for the Harness UI
```

#### Step 2: Define `metadata.json` <a href="#step-2-define-metadatajson" id="step-2-define-metadatajson"></a>

```json
{
  "name": "my custom agent",
  "description": "Automatically performs custom analysis and code modifications",
  "version": "1.0.0"
}
```

#### Step 3: Define `pipeline.yaml` <a href="#step-3-define-pipelineyaml" id="step-3-define-pipelineyaml"></a>

```yaml
pipeline:
  clone:
    depth: 50
  stages:
    - name: my-agent
      steps:
        - name: run-agent
          run:
            container:
              image: yourdockerhub/my-agent-plugin:latest
            with:
              working_directory: /harness
              detailed_logging: "true"
            env:
              ANTHROPIC_API_KEY: <+inputs.anthropicKey>
              HARNESS_KEY: <+inputs.harnessKey>
        - name: show-diff
          run:
            shell: bash
            script: |
              git diff
        - name: create-pr
          run:
            container:
              image: himanshu6956/create-pr-plugin:latest
            env:
              HARNESS_KEY: <+inputs.harnessKey>
      platform:
        os: linux
        arch: arm64
  inputs:
    anthropicKey:
      type: secret
      description: Anthropic API key for Claude AI
    harnessKey:
      type: secret
      description: Harness API key for platform operations
    repo:
      type: string
      description: Target repository name
    branch:
      type: string
      description: Target branch
      default: main
```

#### Step 4: Write `wiki.MD` <a href="#step-4-write-wikimd" id="step-4-write-wikimd"></a>

```markdown
# My Custom Agent <a href="#my-custom-agent" id="my-custom-agent"></a>

## Overview <a href="#overview" id="overview"></a>
This agent automatically analyzes your codebase and applies
intelligent modifications using Claude AI.

## Capabilities <a href="#capabilities" id="capabilities"></a>
- Scans source files for patterns
- Applies AI-powered code modifications
- Creates a pull request with changes

## Inputs <a href="#inputs" id="inputs"></a>
| Input | Type | Required | Description |
|-------|------|----------|-------------|
| anthropicKey | secret | Yes | Anthropic API key |
| harnessKey | secret | Yes | Harness API key |
| repo | string | Yes | Repository name |
| branch | string | No | Target branch (default: main) |

## Troubleshooting <a href="#troubleshooting" id="troubleshooting"></a>
- Ensure your Anthropic API key has sufficient credits
- Verify the Harness API key has project-level permissions
```

#### Metadata validation rules <a href="#metadata-validation-rules" id="metadata-validation-rules"></a>

* Directory names: lowercase with hyphens (e.g., `my-custom-agent`)
* Metadata name: lowercase with spaces (e.g., `"my custom agent"`)
* Input names in `pipeline.yaml`: camelCase (e.g., `anthropicKey`)
* Version: semantic versioning (e.g., `"1.0.0"`)

{% hint style="info" %}
**AUTOMATED REVIEW**

Submit your template as a pull request to the agents repository. Automated Claude Code review via GitHub Actions validates your template against naming conventions, security rules, and cross-file consistency requirements.
{% endhint %}

***

### Testing & deployment <a href="#testing-and-deployment" id="testing-and-deployment"></a>

#### Local testing <a href="#local-testing" id="local-testing"></a>

```bash
# Build the plugin binary <a href="#build-the-plugin-binary" id="build-the-plugin-binary"></a>
cd my-agent-plugin
make build

# Set required environment variables <a href="#set-required-environment-variables" id="set-required-environment-variables"></a>
export PLUGIN_WORKING_DIRECTORY="/path/to/test-repo"
export ANTHROPIC_API_KEY="sk-ant-xxxxx"
export PLUGIN_DETAILED_LOGGING="true"

# Run the plugin locally <a href="#run-the-plugin-locally" id="run-the-plugin-locally"></a>
./my-agent-plugin
```

#### Docker testing <a href="#docker-testing" id="docker-testing"></a>

```bash
make build-docker

docker run --rm \
  -v /path/to/test-repo:/workspace \
  -e PLUGIN_WORKING_DIRECTORY=/workspace \
  -e ANTHROPIC_API_KEY=sk-ant-xxxxx \
  yourdockerhub/my-agent-plugin:latest
```

#### Pipeline testing <a href="#pipeline-testing" id="pipeline-testing"></a>

```yaml
pipeline:
  clone:
    depth: 50
  stages:
    - name: test-agent
      steps:
        - name: run-agent
          run:
            container:
              image: yourdockerhub/my-agent-plugin:latest
            with:
              working_directory: /harness
              detailed_logging: "true"
            env:
              ANTHROPIC_API_KEY: <+secrets.getValue("anthropic_api_key")>
      platform:
        os: linux
        arch: arm64
```

#### Deployment checklist <a href="#deployment-checklist" id="deployment-checklist"></a>

* Plugin binary builds without errors
* Docker image builds and runs successfully
* All required inputs are validated in `Exec()`
* Sensitive values (API keys, tokens) are never logged
* Agent works correctly in a Harness CI pipeline
* Template passes `metadata.json` and `pipeline.yaml` validation
* `wiki.MD` provides clear documentation

{% hint style="warning" %}
**SECRET MASKING**

Always mask sensitive values in your plugin logging. Use logrus field masking or redact API keys before printing. Never log the full value of secrets, tokens, or API keys.
{% endhint %}

### Plugin composition patterns <a href="#plugin-composition-patterns" id="plugin-composition-patterns"></a>

#### Pattern 1: Two-stage analysis + fix <a href="#pattern-1-two-stage-analysis-fix" id="pattern-1-two-stage-analysis-fix"></a>

```yaml
steps:
  - name: analyze-failure
    run:
      container:
        image: yourdockerhub/remediation-agent:latest
      with:
        working_directory: /harness
      env:
        ANTHROPIC_API_KEY: <+inputs.anthropicKey>
        HARNESS_KEY: <+inputs.harnessKey>
  - name: apply-fix
    run:
      container:
        image: yourdockerhub/coding-agent:latest
      with:
        working_directory: /harness
        # Automatically discovers task.txt from step 1
      env:
        ANTHROPIC_API_KEY: <+inputs.anthropicKey>
  - name: create-pr
    run:
      container:
        image: himanshu6956/create-pr-plugin:latest
      env:
        HARNESS_KEY: <+inputs.harnessKey>
```

#### Pattern 2: Standalone with custom prompt <a href="#pattern-2-standalone-with-custom-prompt" id="pattern-2-standalone-with-custom-prompt"></a>

```yaml
steps:
  - name: run-agent
    run:
      container:
        image: yourdockerhub/coding-agent:latest
      with:
        working_directory: /harness
        prompt: <+inputs.prompt>
        max_iterations: "300"
      env:
        ANTHROPIC_API_KEY: <+inputs.anthropicKey>
```

#### Pattern 3: Multi-model pipeline <a href="#pattern-3-multi-model-pipeline" id="pattern-3-multi-model-pipeline"></a>

```yaml
steps:
  # Fast analysis with Claude Sonnet
  - name: analyze
    run:
      container:
        image: agent-container:latest
      env:
        MODEL: claude-sonnet-4-5-20250929
        ANTHROPIC_API_KEY: <+inputs.anthropicKey>
      shell: bash
      script: |
        # Quick analysis of repo structure

  # Detailed generation with Claude Opus
  - name: generate
    run:
      container:
        image: agent-container:latest
      env:
        MODEL: claude-opus-4-6
        ANTHROPIC_API_KEY: <+inputs.anthropicKey>
      shell: bash
      script: |
        # High-quality code generation
```

#### Shared containers <a href="#shared-containers" id="shared-containers"></a>

| Container                                    | Purpose                         | Used By                                                   |
| -------------------------------------------- | ------------------------------- | --------------------------------------------------------- |
| `anewdocker25/mydockerhub:coding-agent`      | AI-powered code modification    | Autofix, Code Review, Feature Flag Cleanup, React Upgrade |
| `anewdocker25/mydockerhub:remediation-agent` | Error analysis and diagnosis    | Autofix, Manifest Remediator                              |
| `himanshu6956/create-pr-plugin:latest`       | Multi-SCM pull request creation | Autofix, Code Coverage, React Upgrade                     |

You can reuse these existing agent containers as building blocks. The `coding-agent` and `create-pr-plugin` containers are designed to be composed together in custom agent pipelines.
