# Self-hosting

Run the Nairi agent daemon on your own infrastructure.





Self-hosting runs the agent daemon (`nairid`) on your hardware. Nairi's backend still orchestrates events (Slack mentions, Discord messages, API calls, scheduled jobs) and routes them to your machine over a WebSocket. The actual model calls, file edits, branch pushes, and PR creation all happen on your side.

When to self-host:

* **Data residency / compliance.** Source code, repo contents, and tool output never leave your infrastructure.
* **Private network access.** The agent needs to reach an internal database, VPN-only API, or any service that isn't on the public internet.
* **Free, unlimited capacity.** Self-hosted agents don't count against your plan's concurrency cap.

For the trade-offs against managed runtimes, see [Where and how your agent runs](/help/building-agents/runtime).

<Callout type="warn">
  Run `nairid` in an isolated environment (Docker, VM, dedicated user). When `nairid` runs with `--claude-bypass-permissions`, **anyone with access to your Slack workspace or Discord server can execute arbitrary commands on the host with your user's privileges**. See [Permission modes](#permission-modes) below.
</Callout>

## 1. Install dependencies [#1-install-dependencies]

### Install `nairid` [#install-nairid]

**Via Homebrew (recommended)**

```bash
brew install presmihaylov/taps/nairid
```

To upgrade later:

```bash
brew upgrade presmihaylov/taps/nairid
```

**From source**

Requires Go 1.24+.

```bash
git clone https://github.com/nairiai/nairid.git
cd nairid
make build
```

The compiled binary will be at `bin/nairid`. Either run it directly or copy it onto your `$PATH`.

### Install the rest [#install-the-rest]

* [Git](https://git-scm.com/downloads).
* [GitHub CLI (`gh`)](https://cli.github.com/) — `nairid` shells out to `gh` to push branches and open PRs.
* One AI coding CLI:
  * [Claude Code](https://claude.com/product/claude-code) — recommended. The default agent.
  * [Codex](https://github.com/openai/codex) — OpenAI's CLI.
  * [OpenCode](https://opencode.ai/) — multi-provider option.

You also need a Nairi account. [Sign up here](https://nairi.ai) if you don't have one.

`nairid` runs on **macOS**, **Linux**, and **Windows**, with native binaries for both Intel and ARM.

## 2. Authenticate the tools [#2-authenticate-the-tools]

### Git identity [#git-identity]

The agent commits under whatever identity you configure here. The commits will end up in PRs the agent opens, so pick something recognisable.

```bash
git config --global user.name "Nairi Agent"
git config --global user.email "agent@example.com"
```

### GitHub CLI [#github-cli]

`nairid` uses `gh` for everything GitHub: push, fetch, create pull request. Authenticate with a [fine-grained personal access token](https://github.com/settings/tokens?type=beta) scoped to the repos the agent should touch (Contents, Pull Requests, and Workflows: read/write).

```bash
gh auth login
gh auth status   # verify
```

#### Personal account vs bot account [#personal-account-vs-bot-account]

Two patterns work:

* **Your personal GitHub account.** PRs the agent opens are attributed to you. Simplest for solo use.
* **A dedicated bot account.** Cleaner for teams: every agent commit and PR is clearly machine-authored, and the bot's permissions can be scoped independently. Create the bot account, generate a fine-grained token there, and run `gh auth login --with-token` inside the `nairid` host as that account.

### SSH keys (optional) [#ssh-keys-optional]

If your repos are cloned over SSH instead of HTTPS, make sure the agent's user has the right key loaded:

```bash
ssh-add ~/.ssh/id_rsa
```

`nairid` itself doesn't manage SSH keys — it inherits whatever `git` finds on the host.

### Log into the AI coding CLI [#log-into-the-ai-coding-cli]

Log into whichever CLI you'll use as the agent. `nairid` does not handle these credentials itself. It spawns the CLI as a subprocess and the CLI uses its own stored credentials.

```bash
claude        # Claude Code
codex login   # Codex
opencode auth # OpenCode
```

Each CLI persists credentials in its own location (`~/.claude/.credentials.json` for Claude Code, etc.).

## 3. Clone your repository [#3-clone-your-repository]

`nairid` works in a directory you control. If the agent should operate on a code repo, clone it:

```bash
git clone https://github.com/your-org/your-repo.git
cd your-repo
```

<Callout type="warn">
  **Use a dedicated clone.** `nairid` creates branches, makes commits, and opens PRs autonomously. Don't point it at the same working tree you develop in. Clone the repo into a separate directory (e.g. `~/nairid-workdirs/your-repo`) so the agent's branches and worktrees don't collide with your own.
</Callout>

### No-repo mode [#no-repo-mode]

Skip the clone if you want a chat-only agent that doesn't touch any code. Create an empty directory and `cd` into it. In this mode, `nairid` requires `NAIRI_AGENT_ID` to be set (see [Start `nairid`](#5-start-nairid) below), since it has no repo URL to derive an instance ID from.

#### Multi-repository setup [#multi-repository-setup]

The `nairid` process can only be bound to one repository at startup, but no-repo mode is a clean workaround if the agent should operate across several. Start `nairid` in no-repo mode from a working directory of your choice, then inside it create a `repos/` folder and clone every repository you want the agent to work on:

```bash
mkdir -p ~/nairid-workdir/repos
cd ~/nairid-workdir/repos
git clone https://github.com/your-org/service-a.git
git clone https://github.com/your-org/service-b.git
git clone https://github.com/your-org/web.git

cd ~/nairid-workdir
NAIRI_API_KEY=… NAIRI_AGENT_ID=multi-repo-bot nairid --agent claude --claude-bypass-permissions
```

The agent runs from `~/nairid-workdir` and can `cd` into any of the cloned repos when a task requires it. You'll need to tell the agent which one to pick — usually with a [rule](/help/rules) that names each repo and what it's for, or by mentioning the repo name in the prompt.

`nairid` won't create branches or open PRs automatically in this mode (it doesn't know which repo is "the" repo). The agent handles git operations itself, the same way it would in any working directory it can shell into.

The **Agent API key** authenticates a `nairid` instance to the Nairi backend. It's a separate credential from the **Public API key** you'd use to call `api.nairi.ai/api/public/v1` from your own code.

1. In the [dashboard](https://app.nairi.ai/agents/fleet), create the agent and toggle **Self-hosted** on.
2. Open the [Self-host page](https://app.nairi.ai/self-host).
3. Click **Generate Agent API Key**. The dialog shows the key once — copy it now, because you won't be able to see it again.

### Rotating the key [#rotating-the-key]

On the Self-host page, click **Regenerate**. A warning dialog tells you exactly what will happen:

> *This will invalidate the old key and any running `nairid` instances using the old key will stop working until you update them with the new key.*

If you have multiple self-hosted agents under the same org, all of them share this one key. Update every instance after a regeneration.

## 5. Start `nairid` [#5-start-nairid]

In the repo's directory (or your no-repo working directory):

```bash
export NAIRI_API_KEY="your-api-key-here"

# Optional but recommended.
# Required when running multiple nairid instances on the same machine
# under the same $HOME (e.g. several repos sharing one user account)
# so each instance gets its own worktrees, state, and logs.
# Required in no-repo mode.
# In a git repo, nairid derives a stable ID from the remote URL if unset.
export NAIRI_AGENT_ID="my-agent-id"

# Default: Claude Code, asks for approval on every file edit.
nairid

# Or pick a different agent / pass a model / bypass permissions.
nairid --agent claude --claude-bypass-permissions
```

The agent's ID is shown on its detail page in the dashboard. Use the same value here.

<Callout type="warn">
  **Put `$HOME` on a persistent volume.** `nairid` keeps its config directory at `~/.config/eksecd/` and its worktrees at `~/.eksec_worktrees/`. Inside those it persists:

  * the **state file** (`~/.config/eksecd/instances/<namespace>/state.json`) — tracks in-flight jobs and seen messages so jobs survive a process restart
  * downloaded **artifacts** (rules, MCP configs, skills)
  * **logs** under `~/.config/eksecd/instances/<namespace>/logs/`
  * **git worktrees** under `~/.eksec_worktrees/<namespace>/`

  If you're running `nairid` in a Docker container, a Kubernetes pod, or any environment with an ephemeral filesystem, mount a persistent volume at `$HOME` (or at the two paths above). Without that, every restart wipes the state file and `nairid` can't recover jobs that were in flight — they're abandoned silently, no retry, no notification.
</Callout>

### All command-line options [#all-command-line-options]

```
nairid [OPTIONS]

  --agent=[claude|codex|opencode]          Which AI assistant to spawn (default: claude)
  --claude-bypass-permissions              Skip the per-tool approval prompt (sandbox only)
  --model=MODEL                            Model override (agent-specific, see below)
  -v, --version                            Print version and exit
  -h, --help                               Print help and exit
```

### Per-agent commands [#per-agent-commands]

#### Claude Code (default) [#claude-code-default]

```bash
# Default: acceptEdits mode (asks for approval on every file change)
nairid --agent claude

# Pick a model: sonnet / haiku / opus, or a full pinned version
nairid --agent claude --model sonnet
nairid --agent claude --model claude-sonnet-4-5-20250929

# Bypass permissions (sandbox only)
nairid --agent claude --claude-bypass-permissions
```

#### Codex [#codex]

```bash
# Default: acceptEdits mode
nairid --agent codex

# Bypass approvals (sandbox only)
nairid --agent codex --claude-bypass-permissions

# Default model: gpt-5.5. Any model string accepted.
nairid --agent codex --model gpt-5.5
```

#### OpenCode [#opencode]

OpenCode **only** supports bypass mode. The `--claude-bypass-permissions` flag is **required**.

```bash
# Default model: opencode/claude-sonnet-4-6
nairid --agent opencode --claude-bypass-permissions

# Custom provider/model
nairid --agent opencode --claude-bypass-permissions --model anthropic/claude-3-5-sonnet
```

## Permission modes [#permission-modes]

`nairid` has two modes that decide how the underlying CLI handles tool calls:

### Secure mode (`acceptEdits`, default) [#secure-mode-acceptedits-default]

In an interactive terminal, `acceptEdits` is the mode where the CLI auto-approves file edits but prompts the user for shell commands and other non-edit tool calls. Under `nairid`, **there is no interactive terminal** — no human is sitting there to click "yes" on a prompt. If a tool call would prompt, the call effectively can't happen.

That means **`nairid` itself doesn't enforce approvals**. What actually controls which tools the agent can run is whatever permission config you've set up in the underlying CLI's own config files.

#### Claude Code [#claude-code]

Permissions live in `~/.claude/settings.json` (user-global) or `.claude/settings.json` checked into the repo (project-scoped). The `permissions` block has `allow`, `deny`, and `defaultMode` fields:

```json
{
  "permissions": {
    "defaultMode": "acceptEdits",
    "allow": [
      "Bash(npm install)",
      "Bash(npm test)",
      "Bash(git status)",
      "Bash(git diff:*)",
      "Read(./src/**)",
      "Edit(./src/**)"
    ],
    "deny": [
      "Bash(rm -rf *)",
      "Read(./.env)",
      "Read(./.env.*)"
    ]
  }
}
```

Tool patterns in `allow` are pre-approved; patterns in `deny` are blocked outright. Anything outside the `allow` list falls back to whatever `defaultMode` decides — which for a non-interactive daemon means it won't run.

See the [Claude Code settings docs](https://code.claude.com/docs/en/settings) for the full list of supported tools and pattern syntax.

#### Codex [#codex-1]

Codex has its own permission model configured under `~/.codex/config.toml`. Refer to the [Codex docs](https://github.com/openai/codex) for the current format and supported tools.

#### OpenCode [#opencode-1]

Not supported in secure mode. OpenCode always runs in bypass mode (see below).

### Bypass mode (`bypassPermissions`) [#bypass-mode-bypasspermissions]

The agent runs every tool call without prompting. File edits, shell commands, network calls — all go through immediately.

When you'd want bypass mode:

* The host is sandboxed (Docker container, dedicated VM, throwaway user account).
* No human is online to respond to approval prompts (overnight runs, scheduled jobs).
* You want the agent to truly be autonomous.

<Callout type="warn">
  With bypass mode on, anyone who can mention the agent in Slack or Discord can run arbitrary commands on the host as the user running `nairid`. **Only enable bypass mode in an isolated environment** (Docker, VM, dedicated user) where the host being compromised is acceptable.
</Callout>

Enable bypass with `--claude-bypass-permissions`. The flag is named after Claude's mode but applies to Codex too.

## 6. Configure agent behaviour (optional) [#6-configure-agent-behaviour-optional]

`nairid` inherits configuration from the CLI you ran it under. Anything Claude Code (or Codex / OpenCode) picks up from the filesystem applies to `nairid` too.

| Setting         | Claude Code                                     | OpenCode                                         | Codex                                  |
| --------------- | ----------------------------------------------- | ------------------------------------------------ | -------------------------------------- |
| **Rules**       | individual `.md` files under `~/.claude/rules/` | concatenated into `~/.config/opencode/AGENTS.md` | concatenated into `~/.codex/AGENTS.md` |
| **MCP servers** | `~/.claude/mcp.json`                            | `~/.config/opencode/mcp.json`                    | `~/.codex/mcp.json`                    |
| **Skills**      | `~/.claude/skills/`                             | `~/.config/opencode/skills/`                     | `~/.codex/skills/`                     |

See the [Claude Code settings docs](https://code.claude.com/docs/en/settings) or [OpenCode config docs](https://opencode.ai/docs/config/) for the canonical reference.

### Using dashboard-managed artifacts [#using-dashboard-managed-artifacts]

Even on a self-hosted runtime, you can still attach **rules, skills, and MCP configs** from the [Settings → Artifacts](https://app.nairi.ai/settings/artifacts) page. `nairid` fetches them from the backend at startup and writes them into the agent's config directory. Changes in the dashboard apply on the next restart.

## What `nairid` does at startup [#what-nairid-does-at-startup]

Useful context when something doesn't behave the way you expect.

1. **Acquires filesystem locks** under `~/.eksec_worktrees/<namespace>/.lock` so two instances can't trample each other's state.
2. **Validates the git environment** if you're in a repo. Runs `gh repo view` to confirm `gh` is authenticated and the remote is reachable. Fails loudly if not.
3. **Fetches artifacts** from the Nairi backend (rules, MCP configs, skills) and writes them into the right places on disk.
4. **Loads persisted job state** from `~/.config/eksecd/instances/<namespace>/state.json`. Recovers in-flight jobs that survived a restart.
5. **Pre-creates worktrees** in a small pool so jobs can start fast without paying a `git worktree add` cost on every message.
6. **Connects to the backend over WebSocket.** From here on, `nairid` is listening for messages.

If the backend is unreachable, `nairid` retries with exponential backoff (2s up to 10s) indefinitely.

## Logs [#logs]

`nairid` writes rotating logs to both stdout and disk:

* **Path:** `~/.config/eksecd/instances/<namespace>/logs/`
* **Rotation:** 10 MB per file.
* **Retention:** 7 days for session logs.

When running multiple instances on the same machine, each has its own log directory under its namespace.

## Limitations vs managed runtimes [#limitations-vs-managed-runtimes]

* **Secret vaults aren't supported.** There's no secret proxy on a self-hosted runtime, so the `CCASECRET_*` injection mechanism doesn't apply. Use plain env vars, `direnv`, or your own secret manager for credentials. Be aware that anything in an env var is visible to the agent's process.
* **MCP credentials live on the agent's filesystem.** On managed runtimes the mcp-proxy sidecar holds MCP configs and the agent never sees them. On self-hosted, `nairid` writes MCP configs from the dashboard straight to local files (`~/.claude/mcp.json` etc.) that the CLI reads. Any credentials inlined in those configs are readable by anyone (or anything) with shell access to the host.
* **MCP Marketplace (Composio) isn't supported.** The OAuth-based marketplace requires the managed mcp-proxy. Custom MCP configs that you paste into the dashboard still work.
* **No automatic updates.** You're responsible for keeping `nairid`, the CLI agent, and the OS toolchain current. `brew upgrade presmihaylov/taps/nairid` is the usual move.
* **GitHub token rotation is your problem.** Use a long-lived fine-grained token, or wire up your own rotation (`nairid` re-reads `GH_TOKEN` every minute, so updating it in-place works).

## Running multiple instances on one machine [#running-multiple-instances-on-one-machine]

The default state directory layout assumes one `nairid` per user account. Two instances sharing `$HOME` without explicit IDs will fight over locks and worktrees and one of them will refuse to start.

Set `NAIRI_AGENT_ID` to a unique value per instance:

```bash
# Terminal 1
NAIRI_API_KEY=… NAIRI_AGENT_ID=prod-bot nairid --agent claude --claude-bypass-permissions

# Terminal 2
NAIRI_API_KEY=… NAIRI_AGENT_ID=staging-bot nairid --agent claude --claude-bypass-permissions
```

Each instance gets its own:

* **Worktrees:** `~/.eksec_worktrees/<NAIRI_AGENT_ID>/`
* **State:** `~/.config/eksecd/instances/<NAIRI_AGENT_ID>/state.json`
* **Logs:** `~/.config/eksecd/instances/<NAIRI_AGENT_ID>/logs/`
* **Lock:** `~/.eksec_worktrees/<NAIRI_AGENT_ID>/.lock`

## Troubleshooting [#troubleshooting]

**`nairid` exits immediately with "namespace already locked".**
Another `nairid` is running under the same `NAIRI_AGENT_ID` in this `$HOME`. Either let it finish, or run the second instance with a different `NAIRI_AGENT_ID`.

**`nairid` exits with "git remote unreachable" or "gh not authenticated".**
Run `gh auth status` to verify auth. Run `git ls-remote` to verify the remote is reachable from the host. If using SSH, run `ssh-add -l` to verify a key is loaded.

**Jobs from Slack / Discord aren't reaching the agent.**
On the agent's detail page in the dashboard, check the last heartbeat timestamp. If it's stale, `nairid` isn't connected. Common causes:

* `NAIRI_API_KEY` is wrong, or was rotated and the local copy is stale.
* A firewall is blocking outbound WebSocket connections to `api.nairi.ai`.
* The agent in the dashboard is not marked self-hosted.

**Claude Code asks for permission on every tool call.**
You're in secure mode (the default). Pass `--claude-bypass-permissions` if you want it to run autonomously. Only do this in an isolated environment.

**OpenCode fails with "permission mode required".**
OpenCode only supports bypass mode. Add `--claude-bypass-permissions` to the command.

**GitHub PRs fail to open.**
Check `gh auth status`. The token needs **Contents**, **Pull Requests**, and **Workflows** at read/write on the target repo. Fine-grained tokens default to read-only.

**The agent's branches keep colliding with my own.**
You're pointing `nairid` at your dev clone. Stop. Clone the repo into a dedicated directory and point `nairid` there instead. See [Step 3](#3-clone-your-repository).

**The host is filling up with worktree directories.**
Worktrees live under `~/.eksec_worktrees/<namespace>/`. Recent versions of `nairid` (v0.0.107+) periodically clean up orphans, but very old versions can leak. Upgrade with `brew upgrade presmihaylov/taps/nairid` or rebuild from source.

## Related [#related]

* [Where and how your agent runs](/help/building-agents/runtime)
* [How to deploy an agent](/help/building-agents/how-to-deploy)
* [Vaults](/help/vaults) — managed-only, but worth understanding the alternative
* [`nairid` repository](https://github.com/nairiai/nairid)
* [`nairid` CHANGELOG](https://github.com/nairiai/nairid/blob/main/CHANGELOG.md)

***

*Can't find what you're looking for? Email [support@nairi.ai](mailto:support@nairi.ai).*
