Skip to main content
On every ChatCLI startup an isolated scratch directory is created at $TMPDIR/chatcli-agent-<random>/ and exposed to the agent via the CHATCLI_AGENT_TMPDIR environment variable. This solves two recurring pain points:
  1. The agent needs to create a temporary script (e.g. a .sh for a complex patch) but the /coder boundary blocks writes outside the project tree, forcing the agent to pollute the repository.
  2. When a tool result is truncated and ChatCLI saves the full content to disk with the marker [full output saved to /tmp/...], the agent cannot read that file — the path was outside the workspace boundary.
Starting with this release, the session workspace solves both: the agent has automatic read and write permission inside it, and the tool-result-budget overflow files now live there too.

Layout

$TMPDIR/chatcli-agent-<random>/
  scratch/        <- exposed via CHATCLI_AGENT_TMPDIR
                     (agent can write and read)
  tool-results/   <- destination of EnforceToolResultBudget and
                     TruncateToolResult when an output exceeds
                     the inline limit
The <random> suffix comes from os.MkdirTemp — unique per process. This avoids collisions when multiple chatcli instances run in parallel on the same host.

How does the agent know it exists?

The /agent and /coder system prompt automatically receives a block with:
  • The literal scratch path resolved at startup (the agent doesn’t have to expand any variable).
  • Instructions to use $CHATCLI_AGENT_TMPDIR in shell commands when that’s more convenient.
  • Usage pattern for truncation markers: when reading [full output saved to /tmp/chatcli-agent-XXX/tool-results/...], open the file with read_file instead of re-running the original tool call.
SESSION WORKSPACE & LARGE OUTPUTS

You have an isolated scratch directory for this session, exposed via the
environment variable CHATCLI_AGENT_TMPDIR (current value: /tmp/chatcli-agent-Xy7K3a/scratch).
Both read and write are ALLOWED in this directory and in its subtree.

Use it whenever you need to:
- stage a temporary shell script before exec'ing it
- persist an intermediate artifact between tool calls
- avoid polluting the project tree with one-off files

Scenario 1 — Create and run a temporary script

In /coder, the model has two ways to stage a script:

Direct path (absolute)

<tool_call name="@coder" args='{"cmd":"write","args":{"file":"/tmp/chatcli-agent-Xy7K3a/scratch/patch.sh","content":"BASE64","encoding":"base64"}}' />
<tool_call name="@coder" args='{"cmd":"exec","args":{"cmd":"bash /tmp/chatcli-agent-Xy7K3a/scratch/patch.sh"}}' />
The engine’s validatePath recognises the aux path registered by InitSessionWorkspace and allows the write.

Shell expansion in exec

<tool_call name="@coder" args='{"cmd":"exec","args":{"cmd":"cat > $CHATCLI_AGENT_TMPDIR/patch.sh <<EOF\nset -e\necho hello\nEOF\nbash $CHATCLI_AGENT_TMPDIR/patch.sh"}}' />
The engine doesn’t validate paths here because it’s just a command string — the $CHATCLI_AGENT_TMPDIR expansion happens in the child shell, which inherits the variable from the ChatCLI process.
Do not use $CHATCLI_AGENT_TMPDIR in the file argument of write or patch. validatePath runs filepath.Abs but does not expand environment variables — $CHATCLI_AGENT_TMPDIR/x becomes a literal <cwd>/$CHATCLI_AGENT_TMPDIR/x and gets blocked by the normal boundary check. Use the absolute path that appears in the system prompt for that path.

Scenario 2 — Recover the middle of a truncated output

The tool result budget truncates large tool outputs (> 20K chars by default) and saves the full content to tool-results/. The preview returned to the model contains a marker like:
... [83,450 chars omitted — full output saved to /tmp/chatcli-agent-Xy7K3a/tool-results/budget_tc_3_1.txt]
Before this release, that marker was a dead end — the read tool blocked the path because it was outside the workspace. Now the scratch dir and tool-results/ are on the agent’s read allowlist, so the model just opens the file:
<tool_call name="@coder" args='{"cmd":"read","args":{"file":"/tmp/chatcli-agent-Xy7K3a/tool-results/budget_tc_3_1.txt","start":1200,"end":1500}}' />
And gets exactly the slice it needs, without re-running the original tool call (which would burn tokens again and likely truncate a second time).

Lifecycle

EventAction
NewChatCLI (startup)agent.InitSessionWorkspace(logger) creates the dirs, exports CHATCLI_AGENT_TMPDIR, registers the paths with the validators
Each agent turnAux paths consulted by validatePath (engine) and IsReadAllowed (read validator)
Tool result exceeds budgettool-results/ receives the full content; preview with marker returns to the model
ChatCLI.cleanup() (exit, Ctrl+D, SIGTERM)ws.Cleanup() runs os.RemoveAll on the root dir; the env var is unset

Configuration

VariableDescriptionDefault
CHATCLI_AGENT_TMPDIRRead-only. Absolute path of the session scratch dir, automatically exported to subprocesses.(set at startup)
CHATCLI_AGENT_KEEP_TMPDIRIf true, skip cleanup and keep the files after exit (debugging).false
CHATCLI_BLOCK_TMP_WRITESIf true, blocks the automatic allowlist extension to os.TempDir() and /tmp. Only the session-specific scratch dir stays accessible. Use on multi-user or strict-CI environments.false
To investigate an agent issue after the session ends:
CHATCLI_AGENT_KEEP_TMPDIR=true chatcli
On exit, ChatCLI logs the scratch path and keeps every script and overflow there for you to inspect with ls, cat, grep.

Security

The session workspace does not loosen the agent boundary against sensitive system paths:
  • The scratch dir lives under os.TempDir() ($TMPDIR on macOS, /tmp on Linux), with 0700 permissions on the session root.
  • Validators (engine.validatePath, SensitiveReadPaths.IsReadAllowed) keep enforcing the core protections: blocks on sensitive paths (/etc/shadow, ~/.ssh, ~/.aws/, ~/.gnupg, etc.) and the project boundary.
  • The @webfetch save_to_file confines writes to the scratch dir via filepath.Base + post-resolve check, even if the model passes an absolute path.

/tmp and os.TempDir() allowlisted by default

Starting this release, the entire os.TempDir() and /tmp (when it exists) are added to the read/write allowlist at session init. The practical reason: virtually every modern model, when writing a throwaway script, emits paths like /tmp/check.sh or /tmp/debug-X.py by default. Before, this would trip the boundary check (path "/tmp/check.sh" is outside workspace boundary) and force the model to guess safer paths — leading to silent failures or retries.
Session workspace initialized: /var/folders/xx/chatcli-agent-abc
System temp dirs added to read/write allowlist: [/var/folders/.../T/ /tmp]
  (opt_out_env: CHATCLI_BLOCK_TMP_WRITES=true)
Opt-out for strict sandboxing: if you run on a shared host or CI where /tmp is writable by untrusted siblings, set CHATCLI_BLOCK_TMP_WRITES=true and only the session-isolated scratch dir stays accessible:
# Strict sandbox — back to pre-release behavior
export CHATCLI_BLOCK_TMP_WRITES=true
Expanding to /tmp does not loosen the sensitive-paths block — /etc, ~/.ssh, ~/.aws, ~/.gnupg, etc. remain blocked via sensitivePaths and SensitiveReadPaths. The scope of this change is strictly the OS tmpdir.
Symlinks inside /tmp still go through filepath.EvalSymlinks before allowlist checking — a symlink /tmp/evil/etc/shadow is still blocked by sensitivePaths.

Next Steps

Tool Result Management

How the budget decides what to truncate and where to persist.

Subagent Delegation

Delegate heavy tasks to a subagent with isolated context.

Web Tools

@webfetch save_to_file uses the scratch dir.

Environment Variables

Full list of variables that control the workspace.