Short answers to what readers ask most about this topic.
01What does an unattended coding agent need that an interactive one does not?
Four things: a committing identity with a narrowly scoped token, a workspace it owns and that is reset before every run, a fixed schedule chosen around the model provider's quota reset, and a way to stop — turn limits, a wall-clock cap and a watchdog. The failure mode of an unattended agent is not crashing, it is running forever.
02Why should the host Docker socket never be mounted into an agent container?
Because it converts container isolation into root on the host. An agent that runs shell commands from model output is an untrusted workload with legitimate access, so it should get a filesystem, a network and a memory limit — and nothing that lets it reach the host. If it must build images, give it a rootless builder inside its own container.
03Why can the agent not push to git even though the token is valid?
Many agent runtimes sanitise the environment handed to shell subprocesses, stripping variables whose names look like secrets. That is correct behaviour for a tool running model-authored commands, but it makes environment-variable authentication invisible to the agent. Persist credentials to disk at boot with tight permissions instead, and verify with the variable explicitly unset.
04How should scheduled runs be timed on a free model tier?
Just after the provider's daily quota reset, which is usually a fixed UTC time. A run that starts then gets a full allowance; the same run in the evening gets whatever is left. Space heavy jobs apart too, because two of them in the same window will rate-limit each other more effectively than any external load.
05What resilience does an unattended agent need?
A hard wall-clock cap per run so a hung job cannot block the scheduler, a clean-start step that resets the workspace and removes untracked files, a watchdog with no model access that reaps stuck processes and prunes disk, and split alerting so routine reports and real failures do not arrive on the same channel.
Running the Hermes Agent as an Autonomous Coder on a VPS
A hardened container, cron workstreams, credentials that survive a sanitised environment, and the resilience layer that keeps an unattended agent alive.
Running a coding agent unattended on a small VPS needs four things: a locked-down container with no Docker socket, credentials persisted to disk rather than environment variables, cron jobs scheduled around the model provider's quota reset, and a watchdog that reaps stuck runs before they fill the disk.
In July I put an open-source coding agent on a two-gigabyte VPS and gave it a job: every morning, improve one of my libraries, open a pull request, and merge it if the tests pass. It has been running since, and the interesting part has not been the model at all. It has been everything around it — the container, the credentials, the schedule and the failure handling.
This is the infrastructure half of that project, written as the notes I wish I had on day one. The names are generic because the same shape applies to any agent runtime you might run this way.
What unattended actually requires
An agent you supervise needs a terminal. An agent you do not supervise needs four things you would give a junior colleague, and they are all infrastructure rather than prompting.
An identity. It commits, pushes and opens pull requests as someone, with a token scoped to exactly what that requires and nothing more.
A workspace. A clone it owns, reset to a known state before every run, so yesterday's failure does not contaminate today's.
A schedule. Fixed times, chosen deliberately, because a free model quota is a resource with a clock attached.
A way to stop. Turn limits, wall-clock timeouts and a watchdog, because the failure mode of an unattended agent is not crashing — it is running forever.
Three of those four are things I got wrong first and fixed later, which is why they get most of the space below.
The isolation the whole setup rests on. The agent gets a filesystem, a network and a memory limit, and specifically does not get the host's Docker socket — the one grant that would turn a contained agent into an uncontained one.
The container, and what it deliberately lacks
The hardening here is unremarkable and worth doing anyway, because an agent that runs shell commands from model output is the definition of an untrusted workload with legitimate access.
# docker-compose.yml — the hardening that costs nothing and matters
services:
agent:
image: agent:local
container_name: agent
env_file: .env # chmod 600, never committed
cap_drop: [ALL] # no capabilities at all
security_opt: [no-new-privileges:true]
mem_limit: 1g # a 2 GB box cannot afford a runaway
read_only: false # the workspace needs writes; the host does not
user: "1000:1000" # never root
volumes:
- agent-state:/home/agent/state # one volume, all persistence
networks: [agent-net] # its own network
# deliberately absent: /var/run/docker.sock
volumes: { agent-state: }
networks: { agent-net: }
Two choices are worth calling out. A memory limit is not optional on a small box: without it, one runaway build takes the whole machine down, including whatever else you are hosting. And all persistence goes into a single named volume, which means a rebuild is safe and a restore is one volume copy rather than an archaeology exercise.
Never mount the host Docker socket into an agent container. It is the single grant that converts container isolation into root on the host, and no amount of prompt-level instruction compensates for it. If the agent genuinely needs to build images, give it a rootless builder inside its own container instead.
The credential trap that cost me two days
The most confusing failure of the whole project: the agent could not push to GitHub, while the exact same command run by hand inside the same container worked perfectly. Not a token problem, not a network problem — the token was valid and the network was fine.
# The failure that cost me two days: the agent could not push, while an
# identical command run by hand worked perfectly.
#
# Cause: the runtime sanitises its subprocess environment and strips
# variables matching *_TOKEN and *_API_KEY before handing the shell to
# the agent. Environment-variable auth is therefore invisible to it.
# Fix: persist credentials to DISK at boot, not to the environment.
git config --global credential.helper store
printf 'https://%s:x-oauth-basic@github.com\n' "$GITHUB_TOKEN" \
> ~/.git-credentials && chmod 600 ~/.git-credentials
mkdir -p ~/.config/gh && cat > ~/.config/gh/hosts.yml <<YAML
github.com:
oauth_token: $GITHUB_TOKEN
git_protocol: https
YAML
chmod 600 ~/.config/gh/hosts.yml
# Verified the right way: unset the variable, then try.
env -u GITHUB_TOKEN gh auth status && env -u GITHUB_TOKEN git push
The cause is a security feature. The runtime sanitises the environment it hands to shell subprocesses, stripping variables whose names look like secrets, which is exactly right for a tool that runs model-authored commands. The consequence is that environment-variable authentication is invisible to the agent even though it is visible to you. The fix is to persist credentials to disk at boot, with tight permissions, so the tools find them without needing the environment. The same trap bit me a second time with model provider keys, and the same fix applied.
Test credential fixes by explicitly unsetting the variable first. Running the command with the environment variable still present proves nothing, because that is the path that already worked; unsetting it is what proves the on-disk credential is doing the work.
Scheduling around a quota, not around office hours
With a free or cheap model tier, the schedule is a budget decision. Daily quotas usually reset at a fixed UTC time, and a job that runs just after that reset gets a full allowance, while the same job in the evening gets whatever is left.
Job
Cadence
Why that slot
Main improvement run
Daily, shortly after the quota reset
The most expensive job gets the freshest budget and the least contention
Second improvement run
Daily, early afternoon
Starts from a main branch the morning run already advanced, so it does not duplicate work
Watchdog
Every few hours, no model involved
Reaps stuck processes and prunes disk before either becomes an outage
Daily report
Evening
Summarises the day's commits and merges, and is the only thing I read routinely
The report matters more than it sounds. An unattended agent with no daily summary is a system you will stop trusting within a week, because you have no evidence it did anything. One email a night with commit counts and merged pull requests is the difference between an experiment and a colleague.
The unglamorous truth of autonomous agents: most of the engineering is ordinary operations work — disk, memory, schedules and restarts — on a machine that nobody is watching at three in the morning. Photo: Carl Lender from Sunrise, USA / Wikimedia Commons, CC BY 2.0
The resilience layer
An unattended agent fails in ways an interactive one never does, because nobody presses escape. Four mechanisms cover almost all of it.
A hard wall-clock cap around each scheduled run, so a hung job cannot block the scheduler behind it. Turn limits alone are not enough, because a single stuck tool call consumes no turns.
A clean-start step at boot: reset the workspace to the remote branch and remove untracked files. Failed runs leave debris, and debris makes the next run's test suite fail for reasons that have nothing to do with the code.
A watchdog with no model access at all, reaping long-running build processes, pruning old logs and sessions, and alerting when disk crosses a threshold.
Split alerting: the routine report goes to email, and only genuine failures go to a chat channel. Mixing them trains you to ignore both.
The clean-start step is the one I would add first in any rebuild. A single untracked test file left behind by a crashed run made three consecutive days of results look like regressions, and the code was fine the whole time.
The order I would build it in again
Five steps, and the first two are the ones people skip.
Version-control the whole deployment — Dockerfile, compose file, config, scripts — with secrets excluded and an example file committed in their place.
Get a single manual run working end to end, including the push and the pull request, before adding any schedule at all.
Add credentials the way the agent will actually consume them, and verify with the environment variables unset.
Add one cron job, watch it for three days, and only then add the second.
Add the watchdog and the daily report before you stop paying attention, not after.
If your installer pulls the latest version of the agent at build time rather than a pinned one, add a cache-busting build argument you bump deliberately. Otherwise your version is whatever happened to be current the last time an unrelated change forced a rebuild, which is not a property you want in something that merges its own code.
An autonomous coding agent on a small VPS is ninety per cent ordinary operations engineering and ten per cent model configuration. Contain it properly, give it credentials it can actually use, schedule it around the quota it depends on, and watch it with something dumber than itself — and it becomes a genuinely useful daily contributor rather than a demo that worked once.