Skip to content
Horon

Keeping your files on your disk

9 min readprivacy, local-first

The file you name is the file it edits#

When you point Horon at a repository, nothing is imported first. The agent runs on this computer, and its file tools open the path you gave them: read returns the bytes on disk, edit rewrites them in place, the shell runs in the same directory. There is no copy uploaded ahead of the work and no sync step that has to finish before the first tool call.

horon-agent serve opens two sockets, one for the harness API that clients attach to and one for the daemon behind it. On Unix they are files in the runtime directory; on Windows, named pipes. Both paths are resolved from the runtime directory rather than a fixed location, so the socket follows HORON_AGENT_RUNTIME_DIR, XDG_RUNTIME_DIR, or TMPDIR on macOS. The process opens no listening network port. Clients reach it, and it reaches out only when a request needs the network.

So the arrangement is not a promise about uploading. It is the shape of the thing: the tool that edits your file runs next to the file. That does not promise that nothing leaves. Three things still do, and each gets its own section below: what a model request carries, the account record on horon.ai, and usage telemetry.

What leaves inside a model request#

A model can only reason about what is in the conversation, and the conversation is what gets sent. When a tool reads a file, that file's contents become part of the request. Let it read a .env file and the values in that file are in the request. When a command prints output, the output is in the request too. The honest boundary is not that your files stay on disk, but that whatever the assistant read while answering you goes to the model provider.

The request leaves over HTTPS to the endpoint you configured. In this checkout the only registered provider runtime is the OpenAI-compatible one, so model traffic is an HTTPS call to that base URL. The provider receives the system prompt, the conversation, the tool results, and the reply it is being asked to produce. From that point the relationship is between you and that provider, and its terms apply rather than ours.

The local proxy changes the first hop, not the destination. horonrouter listens on your machine, holds the provider accounts, and makes the upstream call itself. That moves where credentials and logs live. The provider still sees the request.

The account on horon.ai holds who you are, not your work#

Signing in writes a record to Horon's auth worker: a user row, a session row, the linked provider account, verification tokens, passkeys, signing keys, and device codes for the RFC 8628 flow. They sit in the D1 database behind horon-auth, which has nine tables, and they hold the fields any login system keeps. The other two are a rate-limit counter and a log of desktop download events, and neither carries anything about your files.

No table holds a repository, and the sign-in path uploads no file contents. The dashboard reads your account and, when a relay address is configured, your device list. It cannot read your disk, because it has no path to it. The worker also draws no screens: the login form and the device approval page belong to the site, and the worker only answers their requests.

The relay is a separate service and deserves its own sentence. It keeps a conversation event log for messages that travel through it, and a device registry with each device's id, name, platform, declared capabilities, last seen time, and online state. Publish this computer and that entry exists. The relay stores no passwords and no session secrets; it checks tokens against a published key set. Registry and presence updates reach only the account that owns the device.

The edit journal holds bytes, not history#

Each file-writing tool call records the file's before and after state as one mutation unit: one unit per call, one pair per file, contents addressed by SHA-256 so identical bytes are stored once. That record is what undo reads.

It lives in memory, one journal per session, keyed by session id for the life of the process. Nothing is written to disk, nothing is shared between stores, and nothing survives a restart. The default budget is 64 MiB; when a commit pushes past it, whole units are evicted oldest first, and the unit just committed is never evicted by its own commit. One unit larger than the entire budget is still kept while it is the newest.

Undo restores the newest unit only while every file still matches the recorded after-state. If something else touched the file, the undo refuses rather than overwriting, unless you pass force, which names each path it overwrites. Redo lives in the same process memory and dies with it. The journal is a short undo buffer, not a backup.

The transcript is the thing that persists#

Sessions live under the agent's home, ~/.horon-agent unless HORON_AGENT_HOME says otherwise. Current sessions write sessions/<id>/events.jsonl; older ones keep sessions/<id>.json with an append-only .journal.jsonl beside it, and the reader handles both layouts. Logs sit in logs/, and a small registry file records the socket path and pid of each running daemon.

This is the part that stays, and these are ordinary files. Reading both layouts is what keeps a session created before an update from disappearing. You can read them, copy them, or delete them with the same tools you use for anything else. Nothing in the account database is a copy of them.

One command group uploads sessions, and only once you set it up: horon-agent cloud sessions keeps its own config at ~/.horon-agent/cloud_sessions.json and has a sync subcommand that sends session files to the service you configured. Configure nothing, and nothing moves. It is a feature you turn on, not a side effect of using the agent.

What only answers on this computer#

Two sockets are the entire inbound surface. Nothing outside the machine can call the daemon, because there is nothing to call: no listening port, no HTTP server, no tunnel opened on your behalf. The desktop client reaches the socket through its own Rust shell, since a web view cannot open a Unix socket directly, and that round trip is local as well.

Native plugins load into the agent's process. Host and plugin meet over a C ABI and JSON frames in the same address space, and the contract has no network in it. A plugin receives a config blob and two directories.

Usage telemetry is the exception in this section, and it is on by default. It sends coarse events: install and upgrade, version, operating system and architecture, provider and model names, session and tool summaries. Prompts, source code, model responses, and transcripts are not part of it.

Turn it off with HORON_AGENT_NO_TELEMETRY=1, DO_NOT_TRACK, or a ~/.horon-agent/no_telemetry marker file. Sharing full transcripts is a second choice, off by default, and it uploads the complete conversation including prompts, model responses, and source code that appears in them.

Where the line is thinner than it sounds, and what isn't built#

The telemetry document adds a detail worth repeating: the email address of a provider account used for OAuth sign-in may be attached to usage events, which makes those particular records non-anonymous. If that is not a trade you want, the opt-out above takes one line.

Some of this is not built yet, and the gaps are not rhetorical. The desktop app is not released; the download route hands back an artifact address only when the worker has one configured, so today the desktop is source you can build, not software you can install. There is no plugin store, and plugin publishing is unconfigured in the repository. Checkout does not exist; the billing screen says plainly that there are no subscription records and no payment path.

Three checks cover the boundary better than any promise. Look at ~/.horon-agent after a session and see what your machine kept. Read the request your provider receives, which is the conversation plus whatever the tools pulled into it. Then note which parts never need the network: the file tools and the shell work on bytes that are already here, while the model call is the one step that has to travel.