Hermes write_file: When to Write to Which Path, the Rule
Hermes write_file uses three buckets: /tmp/ for ephemeral scripts, /srv/abs-site/src/ for canonical in-tree files, /opt/data/ for operator scripts and backups. Includes the wrong-path failure shape.

A Hermes subagent can produce the right content and still fail the job, simply by writing it to the wrong path. The write_file tool will faithfully create whatever you point it at, including files that no deploy pipeline will ever see, files that overwrite something the system depends on, and files that get wiped by the next reboot. Knowing where to put it is half of writing it.
The rule is short. Three buckets, one obvious one, one obvious exception, and one storage area for things that have to outlive a session.
The rule in one sentence
Hermes write_file writes to /tmp/ for ephemeral scratch, to the in-tree repo path for canonical files that should ship (e.g. /srv/abs-site/src/...), and to /opt/data/ for operator scripts, run configs, and dated .bak backups — never to a repo root or a live service directory.
Each bucket has a different lifecycle, a different owner, and a different failure mode if you put the wrong thing in it.
The three buckets
| Bucket | Lifecycle | Typical contents | If you put the wrong thing here |
|---|---|---|---|
/tmp/ | Reboot-deleted | One-off scripts, test snippets, downloaded blobs, scratch data | Harmless — survives the session, gone tomorrow |
/srv/abs-site/src/... (in-tree) | Git-tracked, deploy-tracked | Posts, components, layouts, styles, schema | Skips review, can break the build, may or may not ship |
/opt/data/scripts/, /opt/data/abs/, /opt/data/backups/ | Operator-managed, persistent | Watchdogs, deploy scripts, backend code, dated backups | Lives forever; silently diverges from the repo |
The mental model: /tmp/ is for thinking, /srv/abs-site/src/ is for shipping, /opt/data/ is for operating.
Bucket 1 — /tmp/ for ephemeral work
Anything disposable goes under /tmp/. A Python one-liner you want to time. A curl pipe you want to inspect. A fixture for a single test. A downloaded tarball you have not decided to keep. The /tmp/ filesystem on this VPS survives the session and is shared across agent processes, but it does not survive a reboot, and nothing reads it back automatically.
The shape:
write_file(path="/tmp/scratch_bench.py", content="...")
write_file(path="/tmp/headers.txt", content="...")
The cost of being wrong here is almost zero. The cost of putting these things anywhere else is that they accumulate.
Bucket 2 — in-tree for canonical files
When the artifact is meant to ship — a new ABS post, a layout fix, a regression script, a CSS token — write it into the repo at its canonical path. For ABS that means under /srv/abs-site/: src/content/posts/ for posts, src/components/ for components, src/layouts/ for layouts, src/styles/ for styles, scripts/ for repo-bundled helpers.
The shape:
write_file(path="/srv/abs-site/src/content/posts/my-new-post.md", content="...")
write_file(path="/srv/abs-site/src/components/MyComponent.astro", content="...")
Two traps.
Trap A — writing to the repo root instead of src/. write_file(path="/srv/abs-site/my-new-post.md", ...) creates a real file at the repo root. Astro’s content loader is glob({ pattern: '**/*.md', base: './src/content/posts' }), so a Markdown file at the repo root is not picked up, not validated by Zod, and not deployed. The build is green. The post is invisible. The fix is to move the file under src/content/posts/. Use the canonical path the first time; do not rely on a follow-up mv.
Trap B — applying a patch on top of an existing file. write_file overwrites the entire target. If you mean to change three lines of src/content.config.ts, the right call is patch (or a write_file with the whole intended file, intentionally), not a write_file to a new path like src/content.config.ts.new and an assumption that something will rename it. New paths next to existing files do not get renamed; they just sit there until the next git clean.
Bucket 3 — /opt/data/ for operators and backups
The /opt/data/ tree is the operator’s home on this VPS — the things the agent runs and the things the agent backs up. Persistent, separate from any single repo, outside the Astro build pipeline.
Subtrees that matter for ABS work:
/opt/data/scripts/— Python and shell helpers.abs_daily_briefing.py,abs_backend_watchdog.sh, the eighttest_abs_*.pyregression scripts,apollo.sh, theabs_news_cron.py. These are not part of the Astro repo; they are operator-owned scripts that call into it./opt/data/abs/— backend code, prototypes, and per-file backups.backend/holds the FastAPI app;prototypes/holds throwaway code; the bare.bakfiles (e.g.image_tracking.json.20260607T000400Z.bak) are dated snapshots taken before risky edits./opt/data/backups/— larger dumps, hash-named snapshots, and DB copies. Treated as cold storage./opt/data/skills/— the procedural memory the agent reads. Do notwrite_filehere unless you mean to add a new skill./opt/data/cron/jobs.jsonand/opt/data/cloudflare-sites/*.yaml— desired-state files. Edits here change behavior across the whole VPS.
The shape for an operator script:
write_file(path="/opt/data/scripts/abs_new_helper.py", content="...")
The shape for a pre-edit snapshot before touching image_tracking.json:
write_file(
path="/opt/data/abs/image_tracking.json.20260714T153000Z.bak",
content="<current contents>"
)
The naming convention is <original-name>.<UTC-ISO-compact>.bak so the sort order is also the chronological order. Files written here are never cleaned up by automation; they accumulate until an operator (human or scheduled cron) prunes them.
The wrong-path failure shape
The classic ABS failure looks like this in the log:
WROTE: /srv/abs-site/my-new-post.md (217 lines)
BUILD: 0 errors, 0 warnings, 14 pages
ROUTES:
src/content/posts/abs-existing.md → /guides/abs-existing/
src/content/posts/abs-other.md → /guides/abs-other/
ROUTES MISSING:
my-new-post ← not under src/content/posts/
The build is green. The page is missing. The post exists on disk. Zod never saw it because the content loader’s glob only looks inside src/content/posts/. Diagnosis takes one command:
find /srv/abs-site -name 'my-new-post.md' -not -path '*/node_modules/*'
# /srv/abs-site/my-new-post.md ← wrong: repo root, not under src/content/posts/
Fix: re-emit to the canonical path. Do not try to teach the loader to scan the repo root; the path layout is the contract.
The mirror-image failure: writing operator scripts to /srv/abs-site/src/.... The build then tries to compile a Python file or a .bak snapshot as part of the site. Both directions of the wrong-path error waste a deploy cycle.
Why this is half the agent’s job
write_file is content-shaped, not intent-shaped. The tool takes a path and a string; it has no opinion about whether the path is canonical, whether the file is meant to ship, or whether the operator will ever find it again. The agent has to form that opinion before it calls the tool.
A useful pre-write checklist:
- Will this ship? Yes → in-tree under the repo, at the path the loader or import graph expects.
- Is this a one-shot script or test? Yes →
/tmp/. Use a name that says what it is; do not pollute the repo. - Is this an operator helper, watchdog, or backed-up file? Yes →
/opt/data/subtree that matches its kind. - Am I overwriting an existing file? If yes, either take a
.baksnapshot first or usepatchwith a targeted diff. - Will I be able to find this file in a week? If the answer is “I would have to grep everything,” the path is wrong.
Quick-reference table
| If you are writing… | Write to… |
|---|---|
| A new ABS post | /srv/abs-site/src/content/posts/<slug>.md |
| An Astro component or layout | /srv/abs-site/src/components/ or /srv/abs-site/src/layouts/ |
| A regression test | /opt/data/scripts/test_abs_<name>.py |
| A watchdog or deploy helper | /opt/data/scripts/<name>.sh or /opt/data/scripts/<name>.py |
| A backend route or service code | /opt/data/abs/backend/... |
| A pre-edit snapshot of a config | /opt/data/abs/<name>.<UTC-compact>.bak |
| A one-off script, probe, or download | /tmp/<descriptive-name> |
| A throwaway experiment | /tmp/ — never in-tree, never under /opt/data/ |
The rule is not a fancy routing policy. It is three buckets with different lifecycles, a failure shape that is easy to recognize, and a habit of asking “where should this live?” before the first byte.
Sources
- Hermes docs: https://hermes-agent.nousresearch.com/docs
- ABS schema:
/srv/abs-site/src/content.config.ts— the publish contract that decides which.mdfiles become routes. - ABS regression suite:
/opt/data/scripts/test_abs_*.py— eight scripts at/opt/data/scripts/, the canonical location for operator-owned tests.
Last verified: 2026-07-14 against the live /srv/abs-site/ tree, the /opt/data/scripts/ directory listing, and the /opt/data/abs/*.bak snapshot naming convention.



Submit a take
Have a different read on this? Drop a comment below — your email isn't published, and I read every one. Nothing leaves the site until I approve it.