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.

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 below.

1. Install dependencies

Install nairid

Via Homebrew (recommended)

brew install presmihaylov/taps/nairid

To upgrade later:

brew upgrade presmihaylov/taps/nairid

From source

Requires Go 1.24+.

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

You also need a Nairi account. Sign up here 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

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.

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

GitHub CLI

nairid uses gh for everything GitHub: push, fetch, create pull request. Authenticate with a fine-grained personal access token scoped to the repos the agent should touch (Contents, Pull Requests, and Workflows: read/write).

gh auth login
gh auth status   # verify

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)

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

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 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.

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

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

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

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.

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 below), since it has no repo URL to derive an instance ID from.

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:

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 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, create the agent and toggle Self-hosted on.
  2. Open the Self-host page.
  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

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

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

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.

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.

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

Claude Code (default)

# 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

# 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 only supports bypass mode. The --claude-bypass-permissions flag is required.

# 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

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

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

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:

{
  "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 for the full list of supported tools and pattern syntax.

Codex

Codex has its own permission model configured under ~/.codex/config.toml. Refer to the Codex docs for the current format and supported tools.

OpenCode

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

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.

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.

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

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.

SettingClaude CodeOpenCodeCodex
Rulesindividual .md files under ~/.claude/rules/concatenated into ~/.config/opencode/AGENTS.mdconcatenated 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 or OpenCode config docs for the canonical reference.

Using dashboard-managed artifacts

Even on a self-hosted runtime, you can still attach rules, skills, and MCP configs from the 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

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

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

  • 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

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:

# 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

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.

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.


Can't find what you're looking for? Email support@nairi.ai.

On this page