Bring Your Own Agent
Add a custom agent to fullsend — or change the configuration of an existing one — from harness file to CI.
This guide uses the fullsend-ai/agents triage agent as a running example.
How agents work
A fullsend agent has two parts:
- Harness file (YAML) — how the agent runs: sandbox image, policy, scripts, skills, credentials, timeouts.
- Agent definition (Markdown) — what the agent does: prompt, tools, model, skills.
The harness is the entry point. fullsend run triage reads the harness, provisions a sandbox, and launches the agent inside it.
fullsend run triage
│
▼
┌── harness/triage.yaml ─────────┐
│ agent: agents/triage.md │ ◄── prompt & tools
│ policy: policies/triage.yaml │ ◄── sandbox rules
│ skills: [issue-labels] │ ◄── domain knowledge
│ pre_script: scripts/pre-... │ ◄── fetch data (before sandbox)
│ post_script: scripts/post-... │ ◄── act on output (after sandbox)
└─────────────────────────────────┘Security model: agents run inside a sandboxed environment. Network policies control which APIs the agent can reach, and binaries restrictions limit which programs can access each endpoint. Pre-scripts run on the trusted runner before the sandbox starts; post-scripts run after it exits.
Minimum viable agent
You need two files — a harness and an agent definition. For the sandbox policy, reuse an existing one (e.g., the triage agent's policy from fullsend-ai/agents):
my-agent-repo/
├── harness/my-agent.yaml # Execution config
└── agents/my-agent.md # Agent promptharness/my-agent.yaml:
agent: agents/my-agent.md
image: ghcr.io/fullsend-ai/fullsend-sandbox:latest
policy: https://raw.githubusercontent.com/fullsend-ai/agents/<sha>/policies/triage.yaml#sha256=abc...
role: my-agent
slug: fullsend-ai-coder
timeout_minutes: 15agents/my-agent.md:
---
name: my-agent
description: One-line description of what this agent does.
tools: Bash(gh,jq)
model: opus
---
You are my-agent. Your job is to [task description].
## Steps
1. Fetch input from environment variables
2. Analyze and process
3. Write JSON result to `$FULLSEND_OUTPUT_DIR/agent-result.json`
Do NOT push code, create issues, or modify anything directly.
Your only output is the JSON result file.When you need a custom policy (e.g., to grant access to additional APIs or restrict binaries), create your own policies/my-agent.yaml. See the triage agent's policy for a working example. Use binaries to restrict which programs can reach each endpoint — intentionally exclude curl from policies that grant credential-bearing access.
Real-world example: the triage agent
The fullsend-ai/agents triage agent is a full production agent. Here's its harness:
agent: agents/triage.md
doc: docs/agents/triage.md
model: opus
image: ghcr.io/fullsend-ai/fullsend-sandbox:latest
policy: policies/triage.yaml
role: triage
slug: fullsend-ai-triage
host_files:
- src: common/env/gcp-vertex.env
dest: /sandbox/workspace/.env.d/gcp-vertex.env
expand: true
- src: ${GOOGLE_APPLICATION_CREDENTIALS}
dest: /tmp/.gcp-credentials.json
- src: ${GCP_OIDC_TOKEN_FILE}
dest: /sandbox/workspace/.gcp-oidc-token
optional: true
- src: env/triage.env
dest: /sandbox/workspace/.env.d/triage.env
expand: true
skills:
- skills/issue-labels
validation_loop:
script: scripts/validate-output-schema.sh
max_iterations: 2
env:
runner:
FULLSEND_OUTPUT_SCHEMA: ${FULLSEND_DIR}/schemas/triage-result.schema.json
forge:
github:
pre_script: scripts/pre-triage.sh
post_script: scripts/post-triage.sh
env:
runner:
GITHUB_ISSUE_URL: ${GITHUB_ISSUE_URL}
GH_TOKEN: ${GH_TOKEN}
sandbox:
GITHUB_ISSUE_URL: "${GITHUB_ISSUE_URL}"
GH_TOKEN: "${GH_TOKEN}"
timeout_minutes: 10Key patterns to note:
host_filescopy credentials from the trusted runner into the sandbox.expand: trueresolves${VAR}references before copying.forge.githubscopes scripts and env vars to GitHub. When running on GitLab, aforge.gitlabblock would take effect instead.validation_loopre-invokes the agent if its JSON output fails schema validation.common/env/gcp-vertex.envis referenced by relative path because both files live in the same repo. If your agent lives in a different repo, reference it by URL (see Remote references) or copy it locally.
Harness field reference
# ── Required ──────────────────────────────────────────────────
agent: agents/my-agent.md # Path to agent definition
role: my-agent # Role name (a-z, 0-9, _, -)
# ── Identity & metadata ──────────────────────────────────────
slug: fullsend-ai-coder # GitHub App credential slug
description: One-line summary # Human-readable description
# ── Composition ───────────────────────────────────────────────
base: harness/common-base.yaml # Inherit from another harness (local or URL)
# ── Sandbox ───────────────────────────────────────────────────
image: ghcr.io/fullsend-ai/fullsend-sandbox:latest
policy: policies/my-agent.yaml
model: opus # LLM model override
providers: # Inference providers (loaded from providers/ dir)
- vertex # References providers/vertex.yaml
# ── Skills & plugins ──────────────────────────────────────────
skills:
- skills/my-skill # Local path or URL with #sha256=...
plugins:
- plugins/gopls-lsp
# ── Scripts (local paths only) ────────────────────────────────
pre_script: scripts/pre-my-agent.sh
post_script: scripts/post-my-agent.sh
agent_input: inputs/my-input.md # File passed as initial input to the agent
# ── Validation ────────────────────────────────────────────────
validation_loop:
script: scripts/validate-output-schema.sh
max_iterations: 2
feedback_mode: stderr # "stderr" (default), "stdout", or "exit_code"
# ── Host files ────────────────────────────────────────────────
host_files:
- src: env/my-agent.env # Runner path (supports ${VAR})
dest: /sandbox/workspace/.env.d/my-agent.env
expand: true # Resolve ${VAR} in contents
- src: ${SOME_CREDENTIAL}
dest: /tmp/.cred.json
optional: true # Skip if missing
# ── Environment ───────────────────────────────────────────────
env:
runner: # Available to pre/post scripts
MY_VAR: "${MY_VAR}"
sandbox: # Available inside sandbox
MY_SETTING: "value"
runner_env: # Legacy (same as env.runner)
MY_VAR: "${MY_VAR}"
# ── Timeouts ──────────────────────────────────────────────────
timeout_minutes: 20
sandbox_timeout_seconds: 300 # 30-600
# ── Remote resources ──────────────────────────────────────────
allowed_remote_resources:
- https://github.com/my-org/agent-library/
allow_runtime_fetch: true
max_runtime_fetches: 10
# ── API servers ───────────────────────────────────────────────
api_servers: # Host-side REST proxies exposed to sandbox
- name: my-api
script: scripts/api-server.sh # Local script that runs the server
port: 8080 # Port the sandbox connects to
env: # Env vars for the server process
API_KEY: "${API_KEY}"
# ── Forge-specific overrides ──────────────────────────────────
forge:
github:
pre_script: scripts/pre-gh.sh
post_script: scripts/post-gh.sh
skills: [skills/github-specific] # Concatenated with top-level
env:
runner:
GH_TOKEN: "${GH_TOKEN}"
gitlab:
pre_script: scripts/pre-gl.sh
# ── Security ──────────────────────────────────────────────────
security:
fail_mode: closed # "closed" (default) or "open"Field merge rules (for base and forge)
| Field type | Behavior |
|---|---|
Scalars (model, pre_script, etc.) | Child wins |
skills, plugins, host_files | Concatenated |
env, runner_env | Merged; child keys win |
validation_loop, security | Child replaces entirely |
allowed_remote_resources | NOT inherited |
Referencing resources: local vs. remote
Local paths resolve relative to the harness file's base directory:
agent: agents/triage.md # → {base}/agents/triage.mdRemote URLs require a #sha256=... integrity hash:
agent: https://raw.githubusercontent.com/org/repo/<sha>/agents/lint.md#sha256=abc...Scripts are local-only — pre_script, post_script, and validation_loop.script must be local paths (they run on the trusted runner). Exception: scripts declared in a base harness fetched via URL are allowed.
Agent definitions
The agent definition is Markdown with YAML frontmatter:
| Field | Purpose |
|---|---|
name | Must match the filename (sans .md) |
description | One-line summary |
tools | Allowed Bash commands (e.g., Bash(gh,jq)) |
model | LLM model |
skills | Skill names to mount |
disallowedTools | Forbidden Bash patterns |
Design principles:
- Agent writes a JSON result file; scripts do all mutations.
- Be specific — define scoring dimensions, thresholds, output schemas.
- Include decision points (branch on confidence, clarity scores, etc.).
Skills
A skill is a directory with a SKILL.md file that teaches the agent domain knowledge:
skills/issue-labels/
SKILL.md # Required: frontmatter + instructions
scripts/ # Optional: helper scripts
references/ # Optional: reference dataReference in the agent frontmatter by name (skills: [issue-labels]) and in the harness by path (skills: [skills/issue-labels]). Skills can also be URLs with integrity hashes.
Scripts
Pre and post scripts run on the trusted runner outside the sandbox.
- Pre-scripts prepare the environment — fetch data, reset state, write files for
host_filesto copy in. - Post-scripts act on agent output — apply labels, post comments, create PRs.
Security: treat agent output as untrusted input. Validate JSON structure, validate field values against allowlists, quote all variables, and limit string lengths.
Harness composition with base
Inherit from an existing harness and override only what differs:
base: https://raw.githubusercontent.com/fullsend-ai/agents/<sha>/harness/triage.yaml#sha256=abc...
model: sonnet
slug: my-org-triage
skills:
- skills/my-enhancement
timeout_minutes: 15Base chains support up to 5 levels. Circular references are detected and rejected. Resolution order: base chain → child overrides → forge selection. See field merge rules for how each field type combines.
Configuring existing agents
You don't need to build from scratch to change how a built-in agent behaves. Use base to inherit the built-in harness and override just the fields you want — then register your configured version so it takes precedence.
Example: add a skill to the code agent
Create a thin harness that inherits from the upstream code agent and adds your skill:
harness/code.yaml:
base: https://raw.githubusercontent.com/fullsend-ai/fullsend/<sha>/internal/scaffold/fullsend-repo/harness/code.yaml#sha256=abc...
skills:
- skills/my-custom-linting # Concatenated with base skills
timeout_minutes: 45 # Override timeout (scalar → child wins)skills/my-custom-linting/SKILL.md:
---
name: my-custom-linting
description: Org-specific linting rules and conventions.
---
# My Custom Linting
[Your skill content...]Register it:
fullsend agent add harness/code.yaml --name code --fullsend-dir .fullsendBecause config-registered agents take precedence over built-in agents on name collision, your code agent replaces the default — with all of the base agent's scripts, policies, host_files, and plugins still inherited.
Example: swap the model for review
base: https://raw.githubusercontent.com/fullsend-ai/fullsend/<sha>/internal/scaffold/fullsend-repo/harness/review.yaml#sha256=abc...
model: sonnetExample: add org-specific environment variables
base: https://raw.githubusercontent.com/fullsend-ai/fullsend/<sha>/internal/scaffold/fullsend-repo/harness/coder.yaml#sha256=abc...
env:
runner:
JIRA_TOKEN: "${JIRA_TOKEN}" # Merged with base env; child keys win
sandbox:
JIRA_PROJECT: "MYPROJ"What you can configure
Any harness field can be overridden. The field merge rules determine how your overrides combine with the base:
- Change model, timeout, image, scripts — scalars replace the base value.
- Add skills, plugins, or host_files — your entries are concatenated with the base's.
- Add or override env vars — maps are merged; your keys win on collision.
- Replace validation or security config — child replaces the entire block.
Registering your agent
Register agents in config.yaml so fullsend discovers them. Both per-repo (.fullsend/config.yaml) and per-org configs support the agents: list.
The examples above show customizing built-in agents via base. If you've built an entirely new agent from scratch, register it the same way — just point to a local harness instead of a URL.
CLI
# Add (auto-pins URL with SHA256):
fullsend agent add \
https://github.com/fullsend-ai/agents/blob/main/harness/triage.yaml \
--fullsend-dir .fullsend
# Add local:
fullsend agent add harness/my-agent.yaml --name my-agent --fullsend-dir .fullsend
# List / update / remove:
fullsend agent list --fullsend-dir .fullsend
fullsend agent update triage <sha> --fullsend-dir .fullsend
fullsend agent remove triage --fullsend-dir .fullsendPer-repo config (.fullsend/config.yaml)
version: "1"
roles: [triage, coder, review, my-agent]
agents:
- https://raw.githubusercontent.com/fullsend-ai/agents/<sha>/harness/triage.yaml#sha256=abc...
- name: my-cool-agent
source: harness/my-cool-agent.yaml
allowed_remote_resources:
- https://raw.githubusercontent.com/fullsend-ai/fullsend/
- https://raw.githubusercontent.com/fullsend-ai/agents/Per-org config
version: "1"
dispatch:
platform: github
defaults:
roles: [triage, coder, review, my-agent]
agents:
- https://raw.githubusercontent.com/fullsend-ai/agents/<sha>/harness/triage.yaml#sha256=abc...
- name: my-cool-agent
source: harness/my-cool-agent.yaml
allowed_remote_resources:
- https://raw.githubusercontent.com/fullsend-ai/fullsend/
- https://raw.githubusercontent.com/fullsend-ai/agents/
repos:
my-repo:
enabled: trueNotes:
- URL entries are automatically pinned with
#sha256=...byfullsend agent add. - URLs must be covered by
allowed_remote_resourcesin the same config. - On name collision, config-registered agents take precedence over built-in agents.
- Per-repo config is read from the base branch, not from PR branches.
Migrating from customized/
Older versions of fullsend used a customized/ directory overlay to extend and configure agents — you would copy an upstream harness into customized/harness/ and edit it in place. Starting with fullsend v0.27.0, this mechanism is deprecated in favor of the base: composition and config-driven registration described in this guide (ADR 0064).
If you have existing files in customized/, the fullsend agent migrate-customizations command (v0.30.0+) automates the conversion to config-driven agents.
Preview what would change:
fullsend agent migrate-customizations --fullsend-dir .fullsend --dry-runRun the migration (creates a PR with the changes):
fullsend agent migrate-customizations --fullsend-dir .fullsend --repo owner/repoThe tool classifies each override and takes the appropriate action:
| Override type | Detection | Action |
|---|---|---|
| Dead | Agent already registered in config | Delete customized/ files |
| Custom | Not in upstream scaffold | Move files to regular directories, register local path in config |
| Modified | Standard scaffold agent, not yet in config | Generate a base: composition harness with the minimal diff, register in config |
For modified agents, the migration produces exactly the kind of thin base: harness shown in Configuring existing agents — only the fields that differ from upstream are included.
Advanced: custom identity
By default, agents authenticate using shared fullsend GitHub Apps via the slug field. If you need your own GitHub App — for custom permissions, compliance, or branding — you can run a standalone mint. Follow the Standalone mint guide to set one up.
Once your standalone mint is running, configure your agent to use it:
Reference your role in the harness:
yamlrole: my-role slug: my-org-my-roleSet
FULLSEND_MINT_URLin your repo to point to your standalone mint.
The standalone mint serves custom roles locally while proxying built-in roles to the hosted mint.
Troubleshooting
| Symptom | Fix |
|---|---|
| Agent crashes at 0s | Sandbox can't reach Vertex AI — check ANTHROPIC_VERTEX_PROJECT_ID and CLOUD_ML_REGION |
| "role field is required" | Add role: to harness |
| Agent can't find input files | Pre-script output paths must match host_files entries |
| Network policy blocks requests | Check openshell-sandbox.log for BLOCKED lines |
| Schema validation fails | Check agent transcript vs. expected schema |
| Agent not found | Verify registration: fullsend agent list |
References
- fullsend-ai/agents — reference implementation used throughout this guide
- Configuring with skills — creating and managing skills
- Configuring with AGENTS.md — repo-level instructions for all agents
- Standalone mint — custom agent roles and identity
