Skip to content

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

/write-file docs/reports/sec-filings.md
```markdown
# SEC Filings Guide for Quants
...
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:

Done — here are the four reports you asked for.

/write-file docs/q1.md
```markdown
# Q1
...

/write-file docs/q2.md

# Q2
...

/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