The /write-file action¶
/write-file is the one action verb that crosses from "agent
manipulating board state" into "agent producing artifacts". It is also
the verb with the most ways to fail quietly. This page covers the
behaviour you'll actually run into.
For the catalogue entry, see ../concepts/action-verbs.md. For the user-facing UI to manage the resulting files, see ../board/projects-and-workspaces.md.
What it does¶
When the engine applies this action it resolves a destination through a
**four-tier cascade**, trying the most specific scope first and falling
back so the content is never lost:
1. **Project workspace** — if the issue is attached to a project, the
file lands in `project.working_dir` (when set) or in the Trimus-managed
`<trimus-home>/companies/<cid>/projects/<pid>/files/`. Records a
`project.file_written` activity entry.
2. **Company files** — if the issue has no project, the file lands in the
company's files directory (`<company-home>/files/`). Records a
`company.file_written` entry.
3. **Instance files** — if neither project nor company can be resolved,
the file lands in `$TRIMUS_HOME/files/`. Records an
`instance.file_written` entry.
4. **Issue comment (last resort)** — if every filesystem tier fails the
write (a permissions error, say), the engine posts the file body back
as an issue comment inside a collapsible `<details>` block, with a
`file.written_as_comment` activity entry, so the artifact survives even
when no directory was writable.
> **This is the big change from older Trimus.** Earlier versions dropped
> `/write-file` entirely when the issue had no project (`write_file
> dropped: issue <id> has no project`). That is **no longer true** — a
> project-less write falls through to the company or instance files
> directory. If you read advice that says "no project, no file", it is
> stale.
Before writing at any tier, the engine validates the path and size (next
section). The agent does **not** get a confirmation back in the same turn
— its reply ships before the action is applied. On a successful write the
engine posts a **confirmation comment on the issue** naming where the file
was saved (project workspace, Company Files, or instance Files) and how to
reach it in the UI (#365), so the agent sees it on its next heartbeat and a
human can find it without digging through the workspace. Files saved to the
company/instance store appear under the recursive **Files** browser, nested
paths and all.
## Path and size rules
Enforced by `home.ValidateProjectFilePath`, `home.SafeJoinWithinDir`, and
`home.WriteProjectFile`:
- Path must be **relative**. Absolute paths (`/etc/passwd`, `C:\foo`) and
anything starting with `/` or `\` are rejected.
- **No traversal** — `..` segments are rejected.
- **No null bytes**, and no empty or bare-`.` path segments.
- After joining, a defense-in-depth check confirms the result is still
inside the workspace dir (`path resolves outside workspace`) — even if
validation missed something.
- Subdirectories are auto-created (`mkdir -p`).
- **Per-file cap: 4 MiB** (`content exceeds 4194304 byte limit`).
- The opening fence's language hint (` ```markdown `, ` ```json `) is
discarded; the body is captured verbatim. The closing fence must be
exactly three backticks on a line by themselves.
- Files are written `0600`; directories `0700`.
## When you'd use it
- "Write a report and save it to the project workspace."
- "Generate a config file for this project."
- "Produce a markdown design doc."
- "Capture findings as a persisted artifact."
In short: any deliverable that should outlive the issue thread.
## When you'd NOT use it
- The artifact is small and ephemeral — a comment is fine.
- The artifact is intermediate scratch the agent doesn't need to keep —
don't pollute the workspace.
- The user wants **structured, keyed** data stored, not a loose file — use
**issue documents** (`PUT /api/issues/{id}/documents/{key}`), which keep
keys + revisions in the database.
## Common pitfalls and how to avoid them
### Body fence missing
The agent emits `/write-file foo.md` and then writes prose instead of a
fenced block. With no fence, the action is skipped and the verb line is
preserved as prose — no file.
**Fix in skill:** include an example that shows the fence. The LLM copies
what it sees.
### Body fence unterminated
The agent forgets the closing ` ``` `. The engine treats it as no body
(rather than swallowing the rest of the reply) and skips the action.
**Fix in skill:** show the opening *and* closing fence, and use a language
hint on the opener (` ```markdown `) — models reliably close fences they
opened with a hint.
### Path rejected
```text
/write-file /etc/passwd # absolute → rejected
/write-file ../../secrets # traversal → rejected
The validator rejects these and the engine logs the rejection to stderr. Both are by design. Note the rejection is logged at the write tier — the engine attempts the next tier only on a filesystem failure, not on a validation failure, so a bad path is simply not written.
File too large¶
Per-file cap is 4 MiB. Larger bodies fail with content exceeds 4194304
byte limit.
Fix: split into multiple files, or for genuinely large artifacts write
through the JSON API (PUT /api/projects/{id}/files/...), where multipart
upload is friendlier (the same 4 MiB cap applies).
It landed somewhere unexpected¶
If you expected the file in the project workspace but it appeared in the
company or instance files dir — the issue probably has no project
attached, so the cascade fell through. Attach the issue to a project
(project_id at create time, or on the issue's properties panel) to keep
writes in the project workspace.
For agents that should be deliberate about this, have your skill instruct
the agent to check whether the issue has a project first and, if not, post
a comment (or /request-approval) asking the user to attach one rather
than writing to a fallback location. The shipped write-project-file
skill demonstrates this.
Multiple files in one turn¶
Emit several /write-file actions back-to-back; each applies
independently:
/write-file docs/q2.md
/status done ```
If one fails (oversized, bad path), the others still apply — failures don't abort the run.
Overwriting¶
/write-file always overwrites. There is no create-only, no
versioning, no merge. To append, the agent must read the current file
(via the project files API), build the new content, and write it back. The
shipped write-project-file skill does not include append helpers — write
your own read-modify-write skill if you need that pattern often.
Operator notes¶
If working_dir is a real git checkout, files an agent writes land as
uncommitted changes in that working tree. Trimus does not git add or
git commit for you — your existing CI / pre-commit / review processes
apply.
For a Trimus-managed fallback path (no working_dir), files land under
$TRIMUS_HOME/companies/<cid>/projects/<pid>/files/ (project tier),
<company-home>/files/ (company tier), or $TRIMUS_HOME/files/ (instance
tier). The operator can archive or expose those directories however they
like.
Where to next¶
- ../concepts/action-verbs.md — the full
catalogue, including
/create-issueif you want a child issue to own the write. - ../board/projects-and-workspaces.md — what users see when an agent writes to their project.
- writing-a-skill.md — patterns for wrapping
/write-filein a domain-specific skill.