$TMPDIR/chatcli-agent-<random>/ and exposed to the agent via the CHATCLI_AGENT_TMPDIR environment variable. This solves two recurring pain points:
- The agent needs to create a temporary script (e.g. a
.shfor a complex patch) but the/coderboundary blocks writes outside the project tree, forcing the agent to pollute the repository. - 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.
Layout
<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_TMPDIRin 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 withread_fileinstead of re-running the original tool call.
Scenario 1 — Create and run a temporary script
In/coder, the model has two ways to stage a script:
Direct path (absolute)
validatePath recognises the aux path registered by InitSessionWorkspace and allows the write.
Shell expansion in exec
$CHATCLI_AGENT_TMPDIR expansion happens in the child shell, which inherits the variable from the ChatCLI process.
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 totool-results/. The preview returned to the model contains a marker like:
tool-results/ are on the agent’s read allowlist, so the model just opens the file:
Lifecycle
| Event | Action |
|---|---|
NewChatCLI (startup) | agent.InitSessionWorkspace(logger) creates the dirs, exports CHATCLI_AGENT_TMPDIR, registers the paths with the validators |
| Each agent turn | Aux paths consulted by validatePath (engine) and IsReadAllowed (read validator) |
| Tool result exceeds budget | tool-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
| Variable | Description | Default |
|---|---|---|
CHATCLI_AGENT_TMPDIR | Read-only. Absolute path of the session scratch dir, automatically exported to subprocesses. | (set at startup) |
CHATCLI_AGENT_KEEP_TMPDIR | If true, skip cleanup and keep the files after exit (debugging). | false |
CHATCLI_BLOCK_TMP_WRITES | If 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 |
Security
The session workspace does not loosen the agent boundary against sensitive system paths:- The scratch dir lives under
os.TempDir()($TMPDIRon macOS,/tmpon Linux), with0700permissions 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
@webfetchsave_to_fileconfines writes to the scratch dir viafilepath.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.
/tmp is writable by untrusted siblings, set CHATCLI_BLOCK_TMP_WRITES=true and only the session-isolated scratch dir stays accessible:
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.