Lyngvi is a hardened, agent-agnostic sandbox for running autonomous coding agents against your own repositories. You drive it from a desktop app — the conversation, the diff and the delivery in one window — or from the command line, which is the same product without the window. The agent gets a working copy, a toolchain and a controlled way out. It never gets your credentials, your git history or a route to the internet.
The app: a conversation per task, the agent’s work as it happens, and the bar above the composer saying what is waiting to be delivered — three files, one of them held back.
Everything the app does is also a command. The CLI ships inside the app and in a package of its own, and both call the same core:
a first run, start to finishsh
./setup.sh --sandbox ~/sandboxes/shop init --repo ~/src/shop✔ sandbox created · profile "default" · 1 repository./setup.sh build && ./setup.sh up✔ localhost/lyngvi-base:2.0.0✔ proxy and gateway up on 10.89.3.0/24 (internal)./setup.sh session new --title "Fix the login redirect" --preset dev✔ session 7f3a2c · container lyngvi-shop-7f3a2c · 8g / 4.0 cpusecho "Find and fix the login redirect bug" | ./setup.sh session ask 7f3a2c … reading src/auth.ts, running the test suite ✗ DENIED api.telemetry.example → not on the allowlist 3 files changed, tests green./setup.sh session review 7f3a2c apply src/auth.ts +12 −3 apply tests/auth.spec.ts +58 −0 hold bin/run.sh EXECUTABLE reject ../secrets.env PATH_TRAVERSAL./setup.sh session apply 7f3a2c --branch agent/fix-login✔ 2 files committed on agent/fix-login
Named after the island in Norse mythology where the wolf Fenrir was bound: a place where great power is kept within defined boundaries.
Making a sandbox, talking to the agent, reading the diff and delivering what you approve are all in the app, and none of it needs the terminal. Every screen below is the real JavaFX app, run against a sandbox with one repository, three conversations and a change waiting to be reviewed. No mock-ups, no retouching.
Every reply shows what the agent read, searched, ran and edited — with +/− counts on each edit — and what the turn cost in tokens and dollars. The strip above the composer says what is waiting for delivery, and that one file is held back.
Lyngvi 2.0.0 · dark theme · sandbox «shop»
The four premises
The product is the boundary
Everything else in Lyngvi is shell around these four. They are the reason an agent can run unattended in a repository you care about.
01
No direct network access
The session container sits on an internal network with no default route. Every outbound connection goes through a Squid proxy with an allowlist, and an explicit blocklist of known exfiltration endpoints is evaluated first.
squid · allowlist + blocklist
02
No access to your .git
The agent works in a copy with a fresh .git — no hooks, no config, no remotes, no credential helper. The whole git attack surface is removed structurally, not by policy.
copied working tree
03
Secrets live in a gateway sidecar
Internal systems are reached through a read-only, allowlisted API that holds the token. The agent only ever sees a hostname. A plugin cannot read the secret: the runtime injects it into a locked upstream object.
never a token in the container
04
Validated, human-approved delivery
Changes come back as a patch, classified on the host file by file, reviewed by you, and committed to a working branch. Never to main, never silently.
apply · hold · reject
Architecture
One core, two front-ends, three layers of containment
All logic lives in a Java core that both the CLI and the JavaFX GUI call in-process. The core is the only thing that talks to git and Podman. Select a part of the diagram to see what it guarantees.
Your machine
JavaFX GUIsetup.sh → lyngvi-cli
lyngvi-core git · podman · statestate/ and .sessions/
Shared stack (podman compose)
Squid proxy → InternetGateway sidecar → Internal systems
internal network · no default route
One container per conversation
/work/<repo>cap_drop ALL/work/<repo>cap_drop ALL/work/<repo>cap_drop ALL
Inside the sandbox
A complete workspace, and nothing more
The agent gets a real development environment — git, a toolchain, a package registry through the proxy — with a strictly bounded view of the world.
What the agent sees
Mounted into the container, and nothing else from the host:
/work/<repo>a working copy with a fresh .git, one mount per repository
/work/outputfiles meant for you, outside the repositories
/work/_attachmentsfiles you sent with a message, read-only
/work/AGENTS.mdthe house rules, generated from the sandbox config
HTTP_PROXY, HTTPS_PROXYthe only way out, to the allowlist
<SYSTEM>_GATEWAY_URLa hostname per configured internal system
What it never sees
Structurally out of reach, not merely discouraged:
Your .git — hooks, config, remotes, credential helpers
Anything else on your filesystem; no home directory, no SSH keys
The host machine or services on its localhost
A token for an internal system — the gateway holds those
sudo, new privileges or any Linux capability
A default route: unknown hosts fail at the proxy, not at your firewall
Agents are interchangeable
Claude Code and OpenCode ship as reference adapters, with subscription login or a token. An adapter is an agent.json plus a command contract, so a third agent is a config file, not a fork. Models running on your own machine (Ollama, LM Studio, MLX) are reached through the proxy on the ports your profile names.
Claude Codesubscription, token or API key · streaming · reasoning · attachments
OpenCodesubscription or key · streaming · local and hosted models
Your ownagent.json, or an adapter.sh in any language
Egress control
Every request the agent makes, answered by a list you own
The blocklist is evaluated first, then the allowlist; anything that matches neither is denied. Try a hostname and see what the proxy would answer.
In the GUI the same list is a table: every host the proxy denied, with a count, and an Allow button that adds it to your section of the allowlist and reloads the proxy.
Delivery
Nothing leaves the sandbox unseen
A delivery is measured from a baseline — the session start, the last delivery, or any delivery in between — built into a patch inside the container, and then classified on the host. Classification is yours, not the agent’s.
01
Baseline
Pick where the diff starts: session start, the last delivery, or an earlier one.
02
Patch
Built with a throwaway index, so uncommitted, unstaged and agent-committed work all come along.
03
Classify
Three buckets on the host, by rule: apply, hold, reject.
04
Review
File by file, as hunks or whole files, in the GUI or the CLI.
05
Branch
Commit or stage on a working branch. Protected branches are refused.
session review 7f3a2cClick a held file to include it — held files are only delivered when you name them.
File
Change
Bucket
src/auth.ts
modified+12 −3
apply
src/session/cookie.ts
modified+4 −1
apply
tests/auth.spec.ts
added+58 −0
apply
bin/run.shEXECUTABLE
addedmode 100755
hold
docs/screenshot.pngBINARY
addedbinary
hold
../secrets.envPATH_TRAVERSAL
added+1 −0
reject
apply
Ordinary text changes, applied when you approve them.
hold
Executables, symlinks, binaries and oversized files. Delivered only when named explicitly.
reject
Path traversal, absolute paths, .git segments, invalid UTF-8, unparseable patches. Never delivered, whatever you click.
Deliver changesgui
The same review in the GUI: what will be applied, what is held back until you tick it, what is rejected — and the diff of the selected file beside it.
The gateway
Reach the intranet without handing over the key
Some work needs the wiki, the issue tracker or an internal API. The gateway is a sidecar that holds the credential and exposes a narrow, read-only, allowlisted view of the system. The agent gets a hostname and a 403 when it asks for something you have not allowed.
What it looks like from inside
agentGET https://wiki.gateway.local/page/48219
gateway200 · text/markdownallowed: space OPS is on the list
agentGET https://wiki.gateway.local/space/FINANCE
gateway403 · not allowednot allowed yet — the human must add it
the gateway answers before any upstream call
./setup.sh gateway instance add wiki --plugin confluence \
--hostname wiki.gateway.local
printf '%s\n' "$TOKEN" | ./setup.sh gateway secret set wiki \
--url https://wiki.example.com --token-stdin
./setup.sh gateway allow wiki add space:OPS
Every system type is a plugin
http is built in and covers any REST API behind a static secret. confluence and jira ship as jars. Your own is a class implementing GatewayPlugin plus a service entry, dropped in with gateway plugin install. The codebase ships with no systems configured — a profile pack adds what your organisation needs.
http · built inconfluence.jarjira.jarGatewayPlugin · your own
What the runtime guarantees, whatever a plugin does
01Unknown hostname → 404, with no upstream call at all
02GET and HEAD only, unless the instance and the profile both allow writes
03The upstream object is pinned to one origin; .. and absolute URLs are refused
04The secret is injected by the runtime and exists as a string nowhere a plugin can read
05The item allowlist is re-read on every lookup and enforced before the handler runs
06An empty allowlist means deny everything
07no-store on every response; set-cookie and hop-by-hop headers stripped
08Masked logging: Authorization values and *_TOKEN variables are never written
09A broken instance fails alone; /healthz reports it as configured: false
The rest of it
Built to be lived in
The boundaries are the point, but a sandbox you avoid is a sandbox that protects nothing.
01
One container per conversation
Sessions cannot see or edit each other’s work, and resource limits mean something. Start, stop, shell in, garbage-collect the idle ones.
02
Profiles instead of hard-coding
Hostnames, allowlists, mirrors, CA names, protected branches, toolchains and language live in profile.json and a profile pack. No organisation is baked into the source.
03
Toolchains as fragments
A slim Debian base plus the fragments you pick: Java 17/21/25, Node 22, Python 3.12, Go, .NET 8, Maven, Gradle, Kotlin.
04
Resource presets
review (2g, 1 cpu), dev (8g, 4), build (16g, 6) — or one derived from your machine. Chosen per session, by what the session is about to do.
05
--json on every command
One JSON document on stdout, documented exit codes, and a line-delimited event stream from session ask. The CLI contract is the public, language-independent API.
06
A desktop app, not a wrapper
Nav rail, streaming chat with tool calls and usage, a file pane, a diff viewer with whole-file mode, and the same review and apply flow as the CLI — the flow itself living in the core both of them call.
07
Pinned and reproducible
Base image, Squid and the gateway base are pinned by digest; the agent CLI version is pinned in its fragment. doctor --deep proves the egress rules from a throwaway container.
08
Attachments and local models
Drop files or paste an image into the composer. Point the sandbox at Ollama, LM Studio or an MLX server on your own machine; the proxy opens just those ports.
09
English and Norwegian
Every user-visible string comes from a resource bundle, in the CLI and the GUI alike. --lang picks one per command.
Hardening, in one table
What is switched off, everywhere
Network
Internal network only; no default route from a session container
Capabilities
cap_drop ALL and no-new-privileges on every container, session containers included
User
Unprivileged agent user with your UID and GID, so mounted files keep their owner
Filesystem
Copied working tree; the gateway runs read-only; nothing else from the host is mounted
No system config, no terminal prompt, no signing, identity fixed by the profile
Credentials
Agent credentials under state/agents/<id>/, mounted only into that agent’s containers; no tokens in .env
Branches
Protected branches from the profile plus the repository default; delivery always lands on a working branch
Images
Base image, Squid and gateway base pinned by digest; CI verifies the pins
Install
From nothing to a running sandbox
The app ships as a jpackage build with the CLI inside it, and the CLI on its own as a jlink image with its own runtime. You do not install Java.
Requirements: macOS or Linux · Podman 5+ · Java 25 only if you build from source
In the app
Install the app, and the «New sandbox» wizard takes you from nothing to a sandbox you can start a conversation in. There is nothing else to install: the CLI ships inside the app, and a sandbox made here links its own cli/ to it — so the same folder is yours to work in from the terminal whenever you want to.
1Folder and repositoriesWhere the sandbox lives, and which repositories a conversation may be given a copy of.
2Base imageThe toolchains the profile asks for and the agents it enables, built once.
3Shared stackThe proxy and the gateway, started and left running while the sandbox is in use.
4CheckThe same checks as doctor: the container runtime, the image, the stack, the repositories and the certificate.
5AgentLog in once, or point the sandbox at a model server of your own. Either can wait until later.
6ReadyThe sandbox is made, and the first conversation can open as the wizard closes.
New sandboxgui
Step one of six. Nothing has been made yet — «Create» writes the sandbox folder and renders its configuration, and the wizard moves on to the image.
macOS: a .dmg, opened the first time with right-click → Open, since the app is not notarised yet. Linux: a .deb or the unpacked app image. Then the conversation, the diff and the delivery are the windows above.