# Installing EONYX: build, run, and configure


**EONYX** is a terminal AI agent for working with code, written in [Go](https://go.dev). It connects directly to any OpenAI-compatible API ([OpenAI](https://openai.com), [OpenRouter](https://openrouter.ai), [DeepSeek](https://www.deepseek.com), [Moonshot](https://www.moonshot.ai), local [Ollama](https://ollama.com) / [llama.cpp](https://github.com/ggml-org/llama.cpp) / [LM Studio](https://lmstudio.ai)) — no external services, no telemetry, no OAuth. One process, one binary, everything local. Here: installation, the first run, and the full configuration.

<!--more-->

## Requirements and installation

### What you need

- **[Go 1.26+](https://go.dev/dl/)** — to build from source (prebuilt binaries don't need it).
- **An API key** for any OpenAI-compatible provider — or a local model server (Ollama, llama.cpp, LM Studio), in which case no key is needed.
- A terminal: [Windows Terminal](https://aka.ms/terminal) / [PowerShell](https://learn.microsoft.com/en-us/powershell/), any Linux shell, macOS Terminal.

### Installation options

{{< tabs defaultTab="0" >}}
{{< tab title="From source" markdown="true" >}}

```bash
git clone https://github.com/avtotor/eonyx
cd eonyx
make build        # builds bin/eonyx with the version from the git tag
```

Or install into `$GOPATH/bin`:

```bash
make install
```

The version is baked into the binary at build time (`git describe --tags`) — there are no hardcoded versions in the code:

```bash
eonyx --version
```

{{< /tab >}}
{{< tab title="Prebuilt binary" markdown="true" >}}

Releases are built by [GoReleaser](https://goreleaser.com) on every `v*` git tag — download the archive for your platform from the [Releases](https://github.com/avtotor/eonyx/releases/latest) page and put the binary in your `PATH`. Linux, macOS, and Windows are supported.

{{< /tab >}}
{{< /tabs >}}

### First run

```bash
cd my-project
eonyx
```

An interactive TUI chat opens. If there is no configuration at any level yet, EONYX creates a working `eonyx.json` in the global directory itself — with two DeepSeek models and one local one (Ollama). All that's left is to set the key:

```bash
export DEEPSEEK_API_KEY=sk-...   # or your own provider, see below
```

{{< admonition type="tip" title="Headless mode for scripts and CI" >}}
The `run` subcommand executes a task without the TUI and then exits:

```bash
eonyx run "describe what this repository does"
eonyx run -m deepseek-v4-pro --yolo "fix the failing test"
```

It also works in a pipe. The argument takes priority over stdin, so when piping don't duplicate the prompt as an argument — otherwise the content from stdin is ignored:

```bash
{ echo "explain the error:"; cat error.log; } | eonyx run
```

The response text is streamed to **stdout**, while tool activity and token statistics go to **stderr**, so you can safely redirect the output into a file or a pipe.

Flags:

- `--model/-m` — the model
- `--session/-s <id>` — continue a specific session
- `--continue/-c` — continue the most recent one
- `--yolo` — disable tool confirmations
- `--json` — events as NDJSON instead of text
- `--system-prompt` — replace the system prompt

Exit codes distinguish outcomes for CI:

- `0` — success
- `2` — usage error
- `3` — API error
- `4` — the task finished, but a tool was blocked by the permission gate
- `5` — hit the `max_steps` limit
- `130` — interrupted with Ctrl+C
{{< /admonition >}}

### The remaining subcommands

| Command          | Purpose                                    |
| ---------------- | ------------------------------------------ |
| `eonyx`          | interactive TUI chat                       |
| `eonyx run "…"`  | headless agent run                         |
| `eonyx sessions` | list of saved sessions                     |
| `eonyx info`     | configuration, data, and project paths     |
| `eonyx config`   | the final (merged) configuration           |
| `eonyx clean`    | remove its own configuration files         |

### Keys and commands in the TUI

| Key       | Action                                       |
| --------- | -------------------------------------------- |
| `Enter`   | send the message                             |
| `Ctrl+J`  | line break in the input field                |
| `↑ / ↓`   | history of your messages                     |
| `Esc`     | interrupt the running turn                   |
| `/`       | command palette (on empty input)             |
| `@`       | file path autocompletion (Tab inserts)       |
| `?`       | help (on empty input)                        |
| `Ctrl+P`  | command palette                              |
| `Ctrl+N`  | switch model                                 |
| `Ctrl+S`  | session manager                              |
| `Ctrl+K`  | skills and MCP servers (on/off)              |
| `Ctrl+O`  | expand/collapse tool output                  |
| `Ctrl+R`  | expand/collapse the model's reasoning        |
| `! <cmd>` | run a shell command                          |
| `Ctrl+C`  | quit                                         |

Slash commands: `/new`, `/clear`, `/add [path]`, `/rename <title>`, `/fork`, `/export`, `/yolo`, `/plan`, `/timestamps`, `/model`, `/sessions [query]`, `/help`, `/quit`.

{{< admonition type="note" title="The '!' command" >}}
The output of a shell command started via `!` is written into the conversation — the model sees what you ran and what changed (the current directory or environment variables, for example).
{{< /admonition >}}

## Configuration

EONYX reads `eonyx.json` from **three levels** and merges them top down — each next level overrides the previous one:

- **Global config**
  - Windows — `%LOCALAPPDATA%\eonyx\eonyx.json`
  - Linux/macOS — `$XDG_CONFIG_HOME/eonyx/eonyx.json` (`~/.config/…` by default)
- **Global data**
  - Windows — the same path
  - Linux/macOS — `$XDG_DATA_HOME/eonyx/eonyx.json` (`~/.local/share/…` by default)
- **Project config**
  - `eonyx.json` or `.eonyx.json` — searched for up the tree from the current directory

The directory where the project config is found becomes the **project root**: a service folder `.eonyx` (exports, logs) appears inside it. The paths can be overridden with the `EONYX_GLOBAL_CONFIG`, `EONYX_GLOBAL_DATA`, and `EONYX_CACHE_DIR` environment variables. To see what was loaded from where: `eonyx info` and `eonyx config`.

{{< admonition type="warning" title="Strict schema" >}}
The parser rejects unknown fields, wrong types, and malformed JSON — EONYX **refuses to start** rather than silently ignoring a typo. For an unknown field the message contains its name and line number; for a wrong type or malformed JSON — a generic error message.
{{< /admonition >}}

### A full eonyx.json example

This is the default config EONYX creates on the first run — it contains every supported top-level key:

```json
{
  "providers": {
    "deepseek": {
      "type": "openai",
      "base_url": "https://api.deepseek.com",
      "api_key": "$DEEPSEEK_API_KEY"
    },
    "local": {
      "type": "openai",
      "base_url": "http://localhost:11434/v1",
      "api_key": ""
    }
  },
  "models": [
    {"id": "deepseek-v4-flash", "provider": "deepseek", "context_window": 1000000, "input_price": 0.14, "output_price": 0.28},
    {"id": "deepseek-v4-pro", "provider": "deepseek", "context_window": 1000000, "input_price": 0.435, "output_price": 0.87},
    {"id": "qwen2.5-coder", "provider": "local", "context_window": 32768, "input_price": 0, "output_price": 0}
  ],
  "default_model": "deepseek-v4-flash",
  "max_steps": 50,
  "permissions": {
    "allowed_tools": []
  },
  "skills": [],
  "mcp": [],
  "hooks": {},
  "report": {
    "database_url": ""
  },
  "system_prompt": "",
  "log_file": "",
  "retry": {}
}
```

### Providers and models

A provider is a way to reach an API. There is only one type so far — `"openai"` — and it covers any OpenAI-compatible endpoint:

```json
"providers": {
  "openrouter": {
    "type": "openai",
    "base_url": "https://openrouter.ai/api/v1",
    "api_key": "$OPENROUTER_API_KEY"
  }
}
```

The `api_key` and `base_url` fields can reference environment variables (`$OPENAI_API_KEY`) — they are expanded after the layers are merged, so secrets are not stored in the file.

A model binds an id to a provider:

| Field            | Purpose                                                              |
| ---------------- | -------------------------------------------------------------------- |
| `id`             | the model name as the API understands it                             |
| `provider`       | a key from the `providers` block                                     |
| `context_window` | context size in tokens — **triggers auto-summarization** of history  |
| `input_price`    | price per 1M input tokens (USD), for the cost counter in the header  |
| `output_price`   | price per 1M output tokens (USD)                                     |

`max_steps` limits the number of tool-call rounds within a single turn (50 by default).

### Permissions

The safe tools — `read`, `ls`, `glob`, `grep`, `git_status`, `git_diff`, `git_log`, `todo_write` — are always allowed. Everything else (writing, editing, bash, git commits, MCP tools…) requires permission:

```json
"permissions": {
  "allowed_tools": ["bash", "write", "edit"],
  "sandbox": true
}
```

- **`allowed_tools`** — the list of tools that run without asking.
- **`sandbox`** — locks the file tools (`read`/`write`/`edit`/`multi_edit`/`ls`) inside the project root; off by default.
- **`--yolo`** (or `/yolo` in the TUI) — disables the gate entirely for the duration of the session.

In the TUI an interactive layer works on top of the static policy: for a tool that isn't in the allowlist, EONYX shows an **Allow once / Allow always (this session) / Deny** dialog. A denial doesn't kill the turn — the model receives it as a tool error and adapts.

{{< admonition type="tip" title="Planning mode" >}}
`/plan` enables a stricter gate: only read tools are available, the agent explores the code and proposes a plan. Once the plan is ready, EONYX asks you: **implement it**, **allow edits**, or **keep planning**.
{{< /admonition >}}

### Hooks

Hooks are shell commands around tool calls. The format: tool name → `before` / `after`:

```json
"hooks": {
  "write": {
    "before": "echo \"writing: $EONYX_INPUT\" >> .eonyx/audit.log",
    "after":  "gofmt -l ."
  },
  "bash": {
    "before": "[ \"$EONYX_CWD\" = \"$HOME\" ] && exit 1 || exit 0"
  }
}
```

Rules:

- **`before`** runs before the tool; a non-zero exit code **blocks** the call.
- **`after`** runs after it; its output is appended to the tool result (the model sees it). Errors in `after` don't break the result.
- Context is passed **through environment variables** rather than by substitution into the command text — injection via the input content is impossible:
  - `EONYX_TOOL` — the tool name
  - `EONYX_PHASE` — `before` / `after`
  - `EONYX_CWD` — the working directory
  - `EONYX_INPUT` — the JSON input
  - `EONYX_RESULT` — the result, only in `after`
- Each hook gets **30 seconds**; hooks run in the same shell session as the `bash` tool.

### Other keys

| Key             | Purpose                                                                     |
| --------------- | --------------------------------------------------------------------------- |
| `skills`        | additional skill directories (see the Skills article)                       |
| `mcp`           | the list of MCP servers (see the MCP article)                               |
| `report.database_url` | [PostgreSQL](https://www.postgresql.org) for the read-only reporting tools<br>empty — the tools are not registered |
| `system_prompt` | a full replacement for the generated system prompt                          |
| `log_file`      | a log path instead of the standard one; supports `$ENV`                     |
| `retry`         | `max_retries` — 3 by default<br>`base_delay_ms` — 500, doubles up to 30 s<br>`response_header_timeout_sec` — 60 |

## Summary

{{< admonition type="abstract" title="Cheat sheet" >}}
- **Installation**
  - `make build` from source or a binary from GitHub Releases
  - check: `eonyx --version`
- **Running**
  - `eonyx` — interactive TUI
  - `eonyx run "task"` — headless, stdout/stderr separated
- **Configuration**
  - the `eonyx.json` cascade: global → data → project
  - strict schema, `$ENV` references for secrets
- **Models**
  - any OpenAI-compatible API
  - `context_window` triggers auto-summarization
  - prices — the cost counter in the header
- **Permissions**
  - reading is always allowed
  - everything else — via `allowed_tools`, the TUI dialog, `--yolo`, or `/plan`
- **Hooks**
  - `before` / `after` around tools
  - context through `EONYX_*` variables
  - `before` can block
{{< /admonition >}}


