Skip to content
By Wayne Sun, Greg Allen, Hector Martinez, Ralph Bean, Hector Martinez Lopez, fmuntean, Francisc Munteanu, Marcus Kok, Marta Anon
Picture of Wayne Sun
Wayne Sun
Picture of Greg Allen
Greg Allen
Picture of Hector Martinez
Hector Martinez
Picture of Ralph Bean
Ralph Bean
Picture of Hector Martinez Lopez
Hector Martinez Lopez
Picture of fmuntean
fmuntean
Picture of Francisc Munteanu
Francisc Munteanu
Picture of Marcus Kok
Marcus Kok
Picture of Marta Anon
Marta Anon

Running agents locally ​

This guide walks through running agents with fullsend on your machine. It sets the base to help you run any agent, default or custom. Both macOS and Linux are supported with Podman as the container runtime.

For building fullsend from source, see the "Building from source" section in CONTRIBUTING.md.

Prerequisites ​

RequirementmacOSLinux
Container runtimePodman Desktop with a running machinePodman
OpenShellPinned per releaseSame
GCP projectAgent Platform API enabled with Claude models enabledSame
GCP credentialsService account key (see section below)Same
GitHub PATClassic PAT with repo scope (see section below)Same

Download the fullsend CLI ​

Download the latest release from GitHub Releases. Pick the archive matching your platform:

PlatformArchive
macOS (Apple Silicon)fullsend_{version}_darwin_arm64.tar.gz
macOS (Intel)fullsend_{version}_darwin_amd64.tar.gz
Linux (x86_64)fullsend_{version}_linux_amd64.tar.gz
Linux (arm64)fullsend_{version}_linux_arm64.tar.gz

Extract and move to a directory in your PATH:

bash
tar xzf fullsend_{version}_darwin_arm64.tar.gz
mv fullsend_{version}_darwin_arm64/fullsend $HOME/.local/bin/

Verify the installation:

Note: the fullsend binary is not signed, on macOS you need to run xattr -d com.apple.quarantine fullsend

bash
fullsend --version

Install OpenShell ​

OpenShell provides the sandbox runtime. There are multiple ways to install it, here we use one similar to how we download it on Fullsend. Use the version fullsend is pinned to — the source of truth is .github/scripts/openshell-version.sh in the fullsend repo at your release tag (also printed on Fullsend workflow runs).

bash
export OPENSHELL_VERSION=0.1.2  # check the pin file for the current version
curl -LsSf https://raw.githubusercontent.com/NVIDIA/OpenShell/v${OPENSHELL_VERSION}/install.sh | OPENSHELL_VERSION=v${OPENSHELL_VERSION} sh
openshell --version

The gateway reads a schema v2 config. With podman, pin the compute driver and the supervisor image that matches the CLI:

toml
# ~/.config/openshell/gateway.toml (Homebrew on macOS: /opt/homebrew/var/openshell/gateway.toml
# is used when this file does not exist)
[openshell]
version = 2

[openshell.gateway]
compute_driver = "podman"

[openshell.drivers.podman]
supervisor_image = "ghcr.io/nvidia/openshell/supervisor:0.1.2"  # match your openshell --version
health_check_interval_secs = 10

Restart the gateway after editing it (brew services restart openshell on macOS). openshell sandbox list answering No sandboxes found. means the CLI reaches it.

Upgrading from OpenShell 0.0.x. 0.1 cannot read 0.0.x gateway state or a schema v1 config, and its installer refuses to replace 0.0.x unless OPENSHELL_ACK_BREAKING_UPGRADE=1 is set. Clean up while the 0.0.x CLI is still installed, and write the new config before installing: the installer starts the gateway straight away.

bash
# 1. With the 0.0.x CLI: delete your sandboxes (they do not carry over)
openshell sandbox delete --all

# 2. Stop the 0.0.x gateway
systemctl --user stop openshell-gateway

# 3. Move the old config and gateway state aside
cfg="${XDG_CONFIG_HOME:-$HOME/.config}/openshell"
state="${XDG_STATE_HOME:-$HOME/.local/state}/openshell"
ts="$(date +%s)"
if [ -f "$cfg/gateway.toml" ]; then mv "$cfg/gateway.toml" "$cfg/gateway.toml.pre-0.1"; fi
for d in gateway tls; do
  if [ -d "$state/$d" ]; then mv "$state/$d" "$state/$d.pre-0.1.$ts"; fi
done

# 4. Write the schema v2 config shown above to "$cfg/gateway.toml", then install
curl -LsSf https://raw.githubusercontent.com/NVIDIA/OpenShell/v${OPENSHELL_VERSION}/install.sh \
  | OPENSHELL_ACK_BREAKING_UPGRADE=1 OPENSHELL_VERSION=v${OPENSHELL_VERSION} sh
openshell sandbox list   # "No sandboxes found." means the new gateway is up

The paths are the XDG defaults, which a Homebrew install (what the installer uses on Apple Silicon macOS) also uses for the gateway state. With Homebrew, stop the gateway in step 2 with brew services stop openshell instead, and start it with brew services start openshell after installing. The rest is unchanged: $cfg/gateway.toml takes precedence over Homebrew's $(brew --prefix)/var/openshell/gateway.toml, and the installer manages Homebrew's TLS under that directory. fullsend recreates its providers and profiles on the next run.

Get Google Cloud Platform credentials ​

Fullsend uses GCP's VertexAI to run inference, so you need a GCP project. After authenticating on gcloud run:

bash
gcloud iam service-accounts create fullsend-local \
  --display-name="Fullsend local agent runner" \
  --project={project-id}

gcloud projects add-iam-policy-binding {project-id} \
  --member="serviceAccount:fullsend-local@{project-id}.iam.gserviceaccount.com" \
  --role="roles/aiplatform.user"

gcloud iam service-accounts keys create fullsend-local-credentials.json \
  --project={project-id} \
  --iam-account=fullsend-local@{project-id}.iam.gserviceaccount.com
chmod 600 fullsend-local-credentials.json

This creates a service account and a local file to authenticate with that account. If you lack permissions give yourself or ask your GCP administrator for permissions or a key for local development.

Create an environment file somewhere secure, current directory or $HOME may be a good option. In our example it is ./fullsend-gcp.env:

bash
# fullsend-gcp.env
ANTHROPIC_VERTEX_PROJECT_ID={project-id}
GOOGLE_CLOUD_PROJECT={project-id}
CLOUD_ML_REGION=global
GOOGLE_APPLICATION_CREDENTIALS=fullsend-local-credentials.json

Tip: if you plan to run the CLI from the container image instead of the native binary, keep the key file and env file in your working directory — the container mounts it as /work and resolves GOOGLE_APPLICATION_CREDENTIALS relative to it.

Get an OpenAI key (GPT on pi or codex) ​

GPT models run on the pi and codex runtimes, with the credential kept out of the sandbox by an OpenShell provider. In CI the runner obtains it from OpenAI with the job's GitHub identity (OpenAI Workload Identity); on your machine there is no such identity, so use an API key from the same OpenAI project. Put it in an environment file, like the GCP settings above — never in the harness YAML and never under env.sandbox (the runner refuses to export it into the sandbox):

bash
# fullsend-openai.env
OPENAI_API_KEY=sk-...

Then pick a GPT model when you run:

bash
fullsend run triage --runtime pi --model openai/gpt-5.6-luna \
  --forge github --env-file fullsend-openai.env --env-file fullsend-triage.env ...

Codex takes the same key and the same harness requirements — swap --runtime pi for --runtime codex.

A committed inference.openai block in the repository's config.yaml is ignored here while OPENAI_API_KEY is set (there is no GitHub OIDC endpoint to exchange with), so the same checkout works in CI and on your machine.

The agent's harness must declare the provider (providers: [openai]; both the definition and the fullsend-openai profile are built into fullsend, nothing needs to be on disk; the fleet's agents declare it) and a sandbox policy (policy: policies/base.yaml — the fleet's agents already have it; a custom harness needs it because the sandbox image's default policy leaves an uninspected route to api.openai.com, which the gateway refuses to carry the credential over). The sandbox only ever sees a placeholder; the provider holding your key belongs to this run, expires an hour after the run at the latest and is deleted when the run ends.

Get a GitHub token ​

Create a fine grained token at GitHub. The permissions depend on the agent to execute, but generally with Write access to Issues, Contents and Pull Requests you cover most of them. If this is not enough, explore the codebase or ask in our community channels.

Clone repositories ​

First clone your target repository locally:

bash
git clone git@github:{org}/{target_repository} /tmp/target-repo

Next clone the repository where the agent definitions live. The canonical source is fullsend-ai/agents. To learn more about custom agents visit Configuring Agents.

bash
git clone --depth 1 https://github.com/fullsend-ai/agents.git /tmp/fullsend-agents/

Run default agents ​

Depending on the agent you want to run you need a different set of environment variables. Check the variables they need in their environment files, referenced in their harness files.

Note: the fleet-clone examples below need --forge github (or --forge gitlab), because the clone's config.yaml sets no forge:. Without a forge, the harness's GitHub settings (ISSUE_URL, the GitHub provider) never apply and the pre-script stops with ISSUE_URL must be set. fullsend run takes the forge from --forge, then forge: in the config.yaml at the root of --fullsend-dir, then CI environment variables (GITHUB_ACTIONS, GITLAB_CI).

Tip: use --no-post-script in the fullsend run calls to avoid side-effects. You can also use --keep-sandbox to debug failures (but remember to remove them).

Tip: fullsend run uses multiple tools on your system. Instead of installing them all, you can use a container image fullsend publishes — see Run from a container below.

Note: to run custom agents set --fullsend-dir to the directory where your custom agent definitions exist.

Triage agent ​

Add to an env file:

bash
# fullsend-triage.env
GH_TOKEN={github-pat}
GITHUB_ISSUE_URL=https://github.com/{org}/{repo}/issues/{issue_num}
bash
fullsend run triage \
  --fullsend-dir /tmp/fullsend-agents/ \
  --target-repo /tmp/target-repo/ \
  --forge github \
  --env-file fullsend-gcp.env \
  --env-file fullsend-triage.env \
  --forge github

Review agent ​

Add to an env file:

bash
# fullsend-review.env
# In CI, REVIEW_TOKEN is auto-minted by the binary when --mint-url is provided.
# For local runs, supply a GitHub PAT manually:
REVIEW_TOKEN={github-pat}
GH_TOKEN={github-pat}
GITHUB_PR_URL="https://github.com/{org}/{repo}/pull/{pr_number}"
PR_NUMBER="{pr_number}"
REPO_FULL_NAME="{org}/{repo}"
# Set by CI on a re-review; leave empty for a first review.
PRIOR_REVIEW_SHA=
PRIOR_REVIEW_PROVENANCE=
bash
fullsend run review \
  --fullsend-dir /tmp/fullsend-agents/ \
  --target-repo /tmp/target-repo/ \
  --forge github \
  --env-file fullsend-gcp.env \
  --env-file fullsend-review.env \
  --forge github

Code agent ​

Add to an env file:

bash
# fullsend-code.env
# In CI, GH_TOKEN and PUSH_TOKEN are auto-minted by the binary when --mint-url is provided.
# For local runs, supply GitHub PATs manually:
GH_TOKEN={github-pat}
PUSH_TOKEN={github-pat}
PUSH_TOKEN_SOURCE=pat
GITHUB_ISSUE_URL=https://github.com/{org}/{repo}/issues/{issue_num}
REPO_FULL_NAME={org}/{repo}
ISSUE_NUMBER={issue_num}
CODE_ALLOWED_TARGET_BRANCHES=main
REPO_DIR=/tmp/repo-dir
GITHUB_WORKSPACE=/tmp/
# Author and committer email for the agent's commits.
GIT_BOT_EMAIL={bot-or-your-email}
bash
fullsend run code \
  --fullsend-dir /tmp/fullsend-agents/ \
  --target-repo /tmp/target-repo/ \
  --forge github \
  --env-file fullsend-gcp.env \
  --env-file fullsend-code.env \
  --forge github

Choosing the runtime ​

Every example above runs on Claude Code, the default runtime. Fullsend also has two opt-in runtimes — pi and codex — and any example on this page runs on either by adding one flag to the same command:

bash
fullsend run triage \
  --fullsend-dir /tmp/fullsend-agents/ \
  --target-repo /tmp/target-repo/ \
  --forge github \
  --env-file fullsend-gcp.env \
  --env-file fullsend-triage.env \
  --forge github \
  --runtime pi

Codex is selected the same way (--runtime codex); pair it with --model openai/gpt-5.6-luna and the OpenAI key above. With an inference.gateway block in auth: api-key mode, codex can instead run --model gateway/<model> with FULLSEND_INFERENCE_GATEWAY_API_KEY set (Codex › Models through an inference gateway).

Everything else about runtimes lives in one place: Agent runtimes for selecting and overriding the runtime, model and effort — per run, or per agent in config.yaml — and Pi › Running it locally or Codex › Running it locally for what a local run on either needs, its models and its troubleshooting.

Remote resource flags ​

When your harness references URL-based skills with transitive dependencies, you can tune resolution limits:

FlagDefaultDescription
--forge(auto-detect)Forge platform to use (github, gitlab). When omitted, resolved from forge: in the config.yaml at the root of --fullsend-dir, then GITHUB_ACTIONS/GITLAB_CI
--max-depth10Maximum dependency depth for transitive resolution (0 disables)
--max-resources50Maximum total remote resources fetched per harness
--offlinefalseReject network fetches; only use cached remote resources

Lock files ​

If a lock.yaml file exists in the fullsend directory, fullsend run uses it to skip re-resolution when the harness has not changed since the lock was generated. Generate or update a lock file with:

bash
fullsend lock code --fullsend-dir /path/to/.fullsend

To lock all harnesses in the directory at once:

bash
fullsend lock --all --fullsend-dir /path/to/.fullsend

When --forge is specified, only that platform variant is locked. When omitted, all forge variants defined in the harness are resolved and the union of their dependencies is locked.

When the lock entry is current (harness SHA256 matches), dependencies are resolved from the local cache without network access. If the harness has changed or a cached artifact is missing, fullsend run falls back to normal network resolution and prints a warning suggesting you re-run fullsend lock.

Use --update to force re-resolution even if the lock entry appears current.

Status notification flags ​

When running agents locally you can optionally enable status comments on the target issue/PR. These flags mirror what the CI workflows pass automatically:

FlagDescription
--run-urlURL of the CI/CD run shown in the status comment
--status-repoRepository (owner/repo) to post status comments on
--status-numberIssue or PR number for status comments
--mint-urlMint service URL for on-demand status comment tokens (default: $FULLSEND_MINT_URL)
--forgeForge platform (github or gitlab); when omitted, resolved from forge: in the config.yaml at the root of --fullsend-dir, then GITHUB_ACTIONS/GITLAB_CI

Example:

bash
fullsend run triage \
  --fullsend-dir /tmp/fullsend-agents/ \
  --target-repo /tmp/target-repo/ \
  --forge github \
  --env-file fullsend-gcp.env \
  --env-file fullsend-triage.env \
  --forge github \
  --status-repo myorg/myrepo \
  --status-number 42 \
  --run-url "https://github.com/myorg/myrepo/actions/runs/12345"

For GitLab repositories, use --forge gitlab instead of --mint-url. The agent resolves its credential through the GitLab role-credential contract and exports GITLAB_TOKEN (and PUSH_TOKEN, for roles with repository-write access) itself; it does not require the mint service. Set the matching per-role secret (Poller/Analyst/Coder, or a registered custom role). A missing role secret fails closed and does not fall back to FULLSEND_FORGE_TOKEN or a directly-set GITLAB_TOKEN. See the operations guide for required environment variables. Self-hosted instances that use a private CA have a separate certificate-provisioning contract.

Status comment behavior is configured via status_notifications in config.yaml. See Status Notifications.

Using an inference gateway (experimental) ​

Experimental. Behaviour may change. This section covers local runs, where the harness ships the extension as a plugin. Hosted (CI) runs use the runner-managed inference.gateway block in .fullsend/config.yaml instead (see the config reference). For the CI setup, see Using an inference gateway. A local run can use that block too with auth: api-key, which reads the gateway key from FULLSEND_INFERENCE_GATEWAY_API_KEY and needs no OIDC endpoint (ADR 0138).

On the pi runtime, an agent can reach its model through an OpenAI/Anthropic-compatible inference gateway (Praxis, LiteLLM, agentgateway, ...). The pi-inference-gateway extension adds a gateway provider to pi, and your harness ships it as a plugin. No fullsend change is needed.

You need the gateway's URL, a key for it, and the id of each model you want, with the API the gateway serves it on:

APIapi valueTypical models
Responsesopenai-responsesGPT
Chat Completionsopenai-completionsOpen-weight models
Messagesanthropic-messagesClaude

The steps edit the agents clone from Clone repositories (/tmp/fullsend-agents/) and use https://gateway.example.com as the gateway.

  1. Add the extension to the harness repository. Download the release, check it, and keep only the code. The block stops before extracting anything if the checksum does not match:

    bash
    (
      set -eu
      ver=0.1.1
      dest=/tmp/fullsend-agents/extensions/inference-gateway
      cd "$(mktemp -d)"
      curl -fsSL -o pi-inference-gateway.tgz \
        "https://github.com/fullsend-ai/pi-inference-gateway/archive/refs/tags/v$ver.tar.gz"
      echo "9d4a9446842f27aa51de99be4f61b6cbc2a6de009ddf04f2dc712c1fb1417d29  pi-inference-gateway.tgz" \
        | shasum -a 256 -c -
      mkdir -p "$dest"
      tar xzf pi-inference-gateway.tgz -C "$dest" --strip-components=1 \
        "pi-inference-gateway-$ver/package.json" "pi-inference-gateway-$ver/LICENSE" \
        "pi-inference-gateway-$ver/src"
      rm "$dest"/src/*.test.ts "$dest"/src/test-fixtures.ts
    )
    text
    pi-inference-gateway.tgz: OK
  2. Add a provider for the key as /tmp/fullsend-agents/providers/gateway.yaml:

    yaml
    ---
    name: gateway
    type: gateway-inference
    credentials:
      INFERENCE_GATEWAY_API_KEY: "${INFERENCE_GATEWAY_API_KEY}"
  3. Allow the gateway host with /tmp/fullsend-agents/profiles/gateway-inference.yaml:

    yaml
    ---
    id: gateway-inference
    display_name: Inference gateway
    description: OpenAI/Anthropic-compatible inference gateway for pi
    category: inference
    credentials:
      - name: api_key
        description: Inference gateway key
        env_vars: [INFERENCE_GATEWAY_API_KEY]
        required: true
        auth_style: bearer
        header_name: authorization
    endpoints:
      - host: gateway.example.com
        port: 443
        protocol: rest
        enforcement: enforce
        allow_uninspected_credentials: true
        rules:
          - allow:
              method: POST
              path: /v1/chat/completions
          - allow:
              method: POST
              path: /v1/responses
          - allow:
              method: POST
              path: /v1/messages
    binaries:
      - "**/node"

    The sandbox only ever holds a placeholder for the key. OpenShell puts the real key on requests to this host and blocks the agent from reaching any other host with it.

  4. Wire them into the harness. In /tmp/fullsend-agents/harness/triage.yaml, add the provider, the profile and the plugin:

    yaml
    providers:
      - vertex-ai
      - openai
      - providers/gateway.yaml
    openshell:
      profiles:
        - profiles/gateway-inference.yaml
    plugins:
      - path: extensions/inference-gateway
        env:
          INFERENCE_GATEWAY_BASE_URL: "https://gateway.example.com"
          INFERENCE_GATEWAY_EXTRA_MODELS: "gpt-6-luna=openai-responses,example-org/open-model=openai-completions,claude-haiku-4-5=anthropic-messages"

    List every model you will use in INFERENCE_GATEWAY_EXTRA_MODELS, as id=api. pi never asks the gateway for its model list inside the sandbox, so a model missing here is unknown to pi.

  5. Add per-model settings (optional). Only needed for a model that needs compat, contextWindow / maxTokens or exclude. Write /tmp/fullsend-agents/extensions/inference-gateway/inference-gateway.json:

    json
    {
      "providers": {
        "gateway": {
          "baseUrlEnv": "INFERENCE_GATEWAY_BASE_URL",
          "models": {
            "claude-opus-5-5": {
              "api": "anthropic-messages",
              "compat": { "supportsMidConvoEffort": false, "supportsStrictTools": false }
            },
            "example-org/open-model": {
              "api": "openai-completions",
              "contextWindow": 262144,
              "maxTokens": 65536
            }
          }
        }
      }
    }
    • Keep "baseUrlEnv": "INFERENCE_GATEWAY_BASE_URL" in the gateway entry. Without it the extension skips the entry with a warning.
    • Put no credential keys in the file (apiKey*, tokenFile, password*). The key comes from the provider.
    • Keep every model in INFERENCE_GATEWAY_EXTRA_MODELS as well.
    • contextWindow / maxTokens are your deployment's limits; the values above are examples.

    See the extension's per-model keys and compat reference. The file ships inside the plugin directory, and the runner checks that directory is unchanged before every iteration, so the agent cannot edit it.

  6. Run with a gateway/ model from the directory that holds your env files from Run default agents:

    bash
    export INFERENCE_GATEWAY_API_KEY=...   # your gateway key
    fullsend run triage \
      --fullsend-dir /tmp/fullsend-agents/ \
      --target-repo /tmp/target-repo/ \
      --forge github \
      --env-file fullsend-gcp.env \
      --env-file fullsend-triage.env \
      --runtime pi \
      --model gateway/gpt-6-luna \
      --no-post-script

    Always write the full gateway/<id>. A bare id gets the default anthropic-vertex/ prefix.

What success looks like ​

The run names the gateway model, sets up the provider and profile, uploads the extension, and the result passes schema validation (excerpt; local paths shortened):

text
    Model: gateway/gpt-6-luna (from --model flag)
    Plugins: /tmp/fullsend-agents/extensions/inference-gateway (pi)
  ✓ Profile imported: gateway-inference (0.1s)
  ✓ Provider ready: gateway (0.1s)
Extension "inference-gateway": uploaded to sandbox
    Turns: 5
  ✓ Agent exited with code 0 (56.2s)
PASS: output validated against schema (0.5s)
    Validation: passed

The same setup passed with an open-weight model on Chat Completions and with claude-haiku-4-5 and claude-sonnet-5 on Messages.

claude-opus-5-5 passed with the per-model file from step 5:

text
    Model: gateway/claude-opus-5-5 (from --model flag)
    Plugins: /tmp/fullsend-agents/extensions/inference-gateway (pi)
  ✓ Profile imported: gateway-inference (0.1s)
  ✓ Provider ready: gateway (0.1s)
Extension "inference-gateway": uploaded to sandbox
    Turns: 6
  ✓ Agent exited with code 0 (32.8s)
PASS: output validated against schema (0.2s)
    Validation: passed

Limits ​

  • Per-model settings go in the plugin's file. The extension's compat, contextWindow / maxTokens and exclude settings live in the inference-gateway.json from step 5, not in the harness env.
  • The base URL and model list come from the harness. The runner exports the plugin's env on every launch, after the agent-writable .env, so plain assignments in .env do not change them. The key is only sent to the profile's host.

Troubleshooting the gateway ​

ErrorCauseFix
WARNING: plugin ".../extensions/inference-gateway" has 1 injection finding(s) in src/config.ts (and src/discovery.ts)The content scan flags patterns in the extension's codeExpected for v0.1.1; the run continues
Warning: Model "<id>" not found for provider "gateway". Using custom model id., then an error such as 404: {"message":"The model `<id>` does not exist ...","code":"model_not_found"}The model is not in INFERENCE_GATEWAY_EXTRA_MODELS, so pi sends it on another listed model's APIAdd <id>=<api> to INFERENCE_GATEWAY_EXTRA_MODELS
Claude: 400 ... messages.1.output_config: Extra inputs are not permittedThe gateway's Claude backend (for example Vertex) rejects pi's mid-conversation effort settingSet "compat": { "supportsMidConvoEffort": false } on that model in inference-gateway.json (step 5)
400 ... disallowed feature structured_outputs for Partner model <id>Your cloud project's policy disallows that feature for the partner modelSet "compat": { "supportsStrictTools": false } on that model in inference-gateway.json (step 5)

Run from a container ​

Instead of downloading the fullsend binary and installing its host-side dependencies, you can run the CLI from the released runner image:

bash
podman pull ghcr.io/fullsend-ai/fullsend-runner:latest

You still need on the host: Podman, OpenShell (the gateway and sandboxes stay on the host; only the CLI moves into the container), GCP credentials, and a GitHub token.

Mount your OpenShell client config and the same paths you would pass to a native fullsend run. --network=host lets the containerized CLI reach the gateway:

bash
podman run --rm -it --network=host \
  -v "$HOME/.config/openshell:/root/.config/openshell" \
  -v /tmp/fullsend:/tmp/fullsend \
  -v /tmp/fullsend-agents:/tmp/fullsend-agents \
  -v /tmp/target-repo:/tmp/target-repo \
  -v "$PWD:/work" \
  ghcr.io/fullsend-ai/fullsend-runner:latest \
  run triage \
    --fullsend-dir /tmp/fullsend-agents/ \
    --target-repo /tmp/target-repo/ \
    --forge github \
    --env-file fullsend-gcp.env \
    --env-file fullsend-triage.env \
    --forge github

The image's working directory is /work, so relative paths in --env-file resolve against the mounted current directory. Run artifacts are written to /tmp/fullsend/ — mount it (or pass --output-dir pointing at a mounted path) to keep them on the host.

macOS: use /private/tmp/... for the /tmp mounts above (and $(pwd -P) instead of $PWD) — see "Container image mounts" in the macOS platform notes. No other change is needed: fullsend detects when it's running inside a container whose loopback doesn't reach the gateway — the case on a macOS Podman machine, where --network=host shares the VM's network namespace, not the Mac's — and transparently points the OpenShell client at host.containers.internal instead. See "Container image" in the macOS platform notes if you need a manual override.

When using --keep-sandbox the CLI within the container is not able to gather podman logs, because the podman binary is not installed within. Run podman logs <sandbox-container> manually on your machine.

On SELinux-enforcing hosts (Fedora/RHEL), bind mounts may need the :z suffix. Prefer adding :z only to the /tmp and $PWD mounts — relabeling ~/.config/openshell touches files the host gateway also reads.

Simulating Fullsend's real customization layers ​

Fullsend automatically aggregates different layers of information before running fullsend run. In case you want to test how customizations impact default agents, or you custom agents, follow the next steps.

If your repository uses config-driven agents registered in .fullsend/config.yaml, pass the target repository's local .fullsend/ directory as --fullsend-dir:

bash
git clone --depth 1 https://github.com/{org}/{repo}.git /tmp/target-repo/

When you execute fullsend run, pass --fullsend-dir as /tmp/target-repo/.fullsend/. See Bring Your Own Agent for the config-driven approach.

Platform notes ​

macOS ​

  • Podman machine: ensure the Podman machine is running (podman machine start) before invoking fullsend. The CLI does not start it automatically.
  • Podman host-gateway: if sandbox creation fails with unable to replace "host-gateway", set host_containers_internal_ip = "192.168.127.254" under [containers] in ~/.config/containers/containers.conf and restart the Podman machine.
  • Architecture mismatch: if your sandbox image uses a different CPU architecture than the host (e.g. amd64 image on an arm64 Mac via QEMU emulation), set FULLSEND_SANDBOX_ARCH=amd64 so the CLI downloads the correct binary. This is not needed in the typical setup where the Podman VM matches the host arch.
  • Container image: --network=host shares the Podman VM's network namespace, not the Mac's, so a gateway configured at 127.0.0.1 is unreachable from inside the container. Fullsend detects this automatically and redirects the containerized CLI to whichever of host.containers.internal (Podman) or host.docker.internal (Docker) is actually reachable (fullsend-ai/fullsend#5261) — no manual steps needed. This depends on one of those names resolving inside the container; if neither does, see the Podman host-gateway note above. To override the detection yourself, set OPENSHELL_GATEWAY_ENDPOINT (e.g. https://host.containers.internal:17670) before running the container — an explicit value here is never overwritten. Always use https://: check openshell gateway list's AUTH column, and if it says mtls, OpenShell will present your client certificate to whatever host this points at, so only point it at a gateway you trust.
  • Container image mounts: bind-mounting /tmp/... paths fails with statfs: no such file or directory on macOS — Podman Desktop's VM shares /Users, /private, and /var/folders via virtiofs, but not the literal /tmp path, and Podman does not resolve the /tmp → /private/tmp symlink before mounting. Use /private/tmp/... (and $(pwd -P) instead of $PWD). The container example above already accounts for this.

Linux ​

  • Rootless Podman: Podman runs rootless by default. Ensure your user has subuids/subgids configured (grep $USER /etc/subuid). If not, run sudo usermod --add-subuids 100000-165535 --add-subgids 100000-165535 $USER && podman system migrate.
  • Gateway connectivity: The sandbox does not move to Ready state and its logs say that it can't connect to the server (gateway). It is likely that you need to bind the gateway to 0.0.0.0. Add OPENSHELL_BIND_ADDRESS on $HOME/.config/openshell/gateway.env and restart the openshell-gateway service.
  • SELinux: on Fedora/RHEL, bind-mounted volumes may need the :z suffix for standalone podman run. OpenShell handles this automatically.

Troubleshooting ​

Sandbox creation fails immediately

  • Check that podman machine start has been run (macOS only)
  • Verify OpenShell is installed: openshell --version
  • Verify the gateway is running: openshell gateway list

Gateway not running or no openshell gateway running

  • Check the openshell-gateway service.
  • Verify it's healthy: curl -sf https://127.0.0.1:8081/healthz
  • Check that it's registered: openshell gateway list

Syntax error: "(" unexpected inside sandbox

  • The macOS Mach-O binary was injected instead of a Linux ELF. Update to fullsend 0.4.0+ which auto-resolves the correct binary, or provide one explicitly with --fullsend-binary

API Error: Error code policy_denied on the first model call (agent exits after ~2 s, 0 tokens)

  • The gateway denied the agent's binary, not the model. Run grep DENIED <run-dir>/logs/openshell-sandbox.log; a line ending in binary '…/claude.exe' not allowed in policy '_provider_vertex_ai' means the Vertex profile lacks **/claude.exe (Claude Code 2.1.2xx runs as claude.exe, even on Linux)
  • For the built-in fullsend-vertex-ai profile, upgrade fullsend: the profile ships in the binary. To try a changed profile locally, copy it under your own id (for example profiles/myorg-vertex-ai.yaml with id: myorg-vertex-ai), list it in openshell.profiles, and point a provider of your own at it (type: myorg-vertex-ai). Files under profiles/ are not imported unless the harness lists them

Agent fails with missing environment variable

  • Check your env file contains all variables listed in the agent's harness YAML (harness/{agent}.yaml in the .fullsend config directory)

arm64 sandbox image pull fails

  • The default :latest tag is amd64-only. Add FULLSEND_SANDBOX_IMAGE=ghcr.io/fullsend-ai/fullsend-sandbox:dev to your env file

unable to replace "host-gateway" on macOS

  • Set host_containers_internal_ip = "192.168.127.254" under [containers] in ~/.config/containers/containers.conf and restart the Podman machine

Debugging network policies locally ​

When customizing network policies, running agents locally lets you inspect sandbox artifacts directly instead of pushing changes and waiting for CI. This section describes what a local fullsend run produces and how to use the output to iterate on network policy allowlists.

Run directory structure ​

Every fullsend run creates a run directory. By default this is under /tmp/fullsend/; override it with --output-dir. Relative --output-dir values are resolved to an absolute path so post-script env vars such as FULLSEND_VALIDATED_ITERATION_DIR do not depend on the process cwd:

bash
fullsend run triage \
  --fullsend-dir /tmp/fullsend-agents/ \
  --target-repo /tmp/target-repo/ \
  --forge github \
  --env-file fullsend-gcp.env \
  --env-file fullsend-triage.env \
  --forge github \
  --output-dir /tmp/my-debug-output

The run directory is printed at the end of every run under Run directory. Its layout:

<run-dir>/
  logs/
    openshell-sandbox.log    # OCSF events: network, process, policy decisions
    openshell-gateway.log    # Gateway-side events
  iteration-1/
    output/                  # Agent output files (agent-result.json, etc.)
    transcripts/             # Agent conversation transcripts (.jsonl)
  metrics.json               # Behavioral metrics (tokens, cost, duration)
  security/
    findings.jsonl           # Security scan findings

Key artifacts for network policy debugging ​

openshell-sandbox.log is the primary artifact. It contains OCSF events for every network connection, HTTP request, process launch, and policy evaluation that occurred inside the sandbox. Each event records whether the connection was allowed or denied and which policy rule applied.

Look for DENIED entries to find connections your policy rejected:

bash
grep -i DENIED <run-dir>/logs/openshell-sandbox.log

Each denied entry includes the destination host, port, and the binary that attempted the connection — enough to decide whether to add the endpoint to your policy.

openshell-gateway.log contains gateway-side events. Useful when a connection fails before reaching the sandbox (e.g., provider misconfiguration or gateway routing issues).

Debugging workflow ​

  1. Start with a restrictive policy. Create or modify a policy YAML that blocks the endpoint you want to test. See Building custom agents — Sandbox policy for the policy format.

  2. Run the agent locally with --keep-sandbox:

    bash
    fullsend run <agent> \
      --fullsend-dir /tmp/fullsend-agents/ \
      --target-repo /tmp/target-repo/ \
      --forge github \
      --env-file fullsend-gcp.env \
      --env-file fullsend-<agent>.env \
      --forge github \
      --keep-sandbox \
      --no-post-script

    --keep-sandbox preserves the sandbox container after the run finishes, letting you exec into it for further inspection. --no-post-script prevents side effects (issue comments, PR creation) while debugging.

  3. Inspect the sandbox log for denied connections:

    bash
    grep -i DENIED <run-dir>/logs/openshell-sandbox.log

    If the analyze-transcript skill is available, you can use the network analysis commands for a structured view:

    bash
    python3 skills/analyze-transcript/analyze-transcript.py network \
      <run-dir>/logs/openshell-sandbox.log
    
    python3 skills/analyze-transcript/analyze-transcript.py netsearch "DENIED" \
      <run-dir>/logs/openshell-sandbox.log
  4. Update your policy to allow the blocked endpoints, then re-run. Each cycle takes only the time to start a sandbox and run the agent — no push or CI wait.

  5. When done, clean up the kept sandbox:

    bash
    openshell sandbox delete <sandbox-name>

    The sandbox name is printed during the run and also shown in the --keep-sandbox warning message at the end.

Entering a kept sandbox ​

When you pass --keep-sandbox, the CLI prints a command to exec into the sandbox:

bash
openshell sandbox exec --tty --name <sandbox-name> -- bash

Inside the sandbox you can inspect the workspace, re-run commands the agent tried, or verify that network access works as expected for a specific host:

bash
curl -sf https://api.example.com/healthz

Tips ​

  • Network summary: openshell-sandbox.log can be large. Use grep -c DENIED to get a count before reading individual entries.
  • Iterate fast: after updating your policy YAML, you do not need to reinstall or reconfigure OpenShell — just re-run fullsend run with the updated policy file.
  • Compare runs: diff two sandbox logs to confirm that a policy change resolved the denials:
    bash
    diff <(grep DENIED run-1/logs/openshell-sandbox.log) \
         <(grep DENIED run-2/logs/openshell-sandbox.log)

See also ​