> ## Documentation Index
> Fetch the complete documentation index at: https://bastani-budgets-duration-enforcement.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Tools

# Built-in tools

Atomic enables these coding tools in normal sessions by default: `read`, `write`, `edit`, `bash`, `find`, and `search`.

## Hashline editing anchors

`read`, `search`, `write`, and successful `edit` results for local text files emit an editable hashline header:

```text theme={null}
[src/example.ts#A1B2]
1:const value = 1;
2:console.log(value);
```

The four-character tag is a snapshot of the file content seen by the model. The `edit` tool accepts hashline scripts anchored to that tag:

```text theme={null}
[src/example.ts#A1B2]
replace 1..1:
+const value = 2;
insert tail:
+// done
```

Supported hashline operations include `replace N..M:`, `replace block N:`, `delete N..M`, `delete block N`, `insert before N:`, `insert after N:`, `insert after block N:`, `insert head:`, and `insert tail:`. Safe lenient variants such as `replace N`, `replace N-M:`, `replace N M:`, `replace N…M:`, bare body rows, and `*** Begin Patch` envelopes are accepted. Bare body rows are auto-prefixed and reported as warnings. `*** Abort` stops parsing the remaining input, while apply-patch sentinels, `@@` hunk headers, bare numeric hunk headers, `delete` bodies, empty `replace`/`insert` bodies, and `-` diff rows are rejected with guidance instead of silently deleting content. Line numbers refer to the original tagged snapshot and do not shift within a call. Parallel `edit` calls that share a `[path#TAG]` are applied as one snapshot-anchored batch (the same merge as multiple hunks under one header), so a later sibling does not fail just because the first write minted a new tag. A follow-up `edit` that arrives after that batch has already committed still uses snapshot recovery for provably non-overlapping drift.

Before writing, Atomic verifies the current file against the tagged snapshot. If the file drifted, `edit` first attempts a snapshot-based recovery for provably non-overlapping external changes and appends a warning when it preserves those changes; unknown tags, overlapping stale edits, and unrecoverable drift fail clearly with the current file hash (and anchor context for drifted files) and leave the file unchanged. Byte-identical no-op edits return a no-op warning without writing, and repeated identical no-ops escalate to an error to stop looped retries. Hashline snapshots are scoped to the active tool/session store, so tags emitted in another session or stale context do not authorize edits. One `edit` input may contain multiple `[PATH#TAG]` sections; Atomic preflights every section before writing, but this is preflight atomicity rather than transactional rollback, so a mid-batch filesystem write failure can leave earlier sections already written. Each successful `write` or `edit` returns a fresh tag for follow-up edits; hashline edit success output is compact and includes the refreshed header plus block-resolution/change metadata while the full diff remains in tool details. Plain `write` success output is likewise compact (`[path#TAG]` plus a byte-count summary), not a full reprint of the file. `write` strips copied hashline headers and `LINE:`/`*LINE:` display prefixes only when the pasted content matches a known current-store snapshot and notes when stripping occurred; complete copied output preserves whether that snapshot had a terminal newline. Literal or unknown hashline-looking content is preserved instead of being stripped.

Hashline anchors must be positive safe integers. Inclusive numeric ranges are limited to 100,000 lines before expansion. An explicit `+TEXT` row that looks like a valid hunk header remains literal and emits a warning. Across whole-file, truncated, and range/offset reads of LF or CRLF text, numbered hashline output treats a terminal newline as a separator rather than an additional synthetic row; genuine blank lines, including one immediately before that newline, remain visible. Truncation totals and continuation selectors count real lines. Files using bare CR line endings retain their existing compatibility behavior and are outside this newline guarantee.

## `bash` and `bashInterceptor`

The `bash` tool executes shell commands in the session workspace, with optional PTY handling. When `pty: true` is requested, local execution uses the bundled Rust-backed PTY session; if the native PTY package is unavailable, Atomic degrades to normal pipe execution. Set `PI_NO_PTY=1` or `ATOMIC_NO_PTY=1` to force normal pipe execution. Foreground results include oh-my-pi-style `timeoutSeconds`, `requestedTimeoutSeconds`, `wallTimeMs`, and non-zero `exitCode` metadata, and preserve overflow output in a temporary `fullOutputPath` when output is truncated.

When explicitly enabled in settings, built-in bash interceptor rules block common shell substitutes for first-class tools (`cat`/`grep`/`find`/in-place `sed`/redirection, etc.) only when the corresponding tool is available. Enabled bash tool calls are also offered to `user_bash` extension handlers before local execution. Atomic checks the original command, the internal-URL-expanded command, configured-prefix forms, `spawnHook`-rewritten commands, and a leading `cd path && command` or `cd path; command`-stripped form only when structured `cwd` was omitted, so interceptors can route commands by effective working directory without overriding explicit `cwd`. The bash schema accepts `cwd`, `env`, `timeout`, and `pty`; `cwd` and `env` are honored by the local executor. Omitting `timeout` uses the 300-second default. An explicit timeout must be finite, greater than zero, and no more than Atomic's deliberate 3600-second ceiling; invalid values fail before execution instead of being defaulted or clamped. Valid fractional values are rounded down with a one-second floor. `bashInterceptor.enabled` defaults to `false`; interception is not auto-enabled.

```json theme={null}
{
  "bashInterceptor": { "enabled": true }
}
```

## `find` and `search`

`find` finds filesystem paths by glob; use `search` when you need content matches instead of path matches. `find.paths` is required and accepts file, directory (including a filesystem root), internal URL, or glob paths; copied quoted paths are normalized, exact filesystem paths with spaces/commas/semicolons are preserved before delimiter expansion, comma/semicolon-joined paths are split when at least one part resolves, whitespace-joined paths are split only when every part resolves, hidden files are included by default, `.gitignore` is respected by default (including nested `.gitignore` files outside a Git checkout), broad scans keep `node_modules`/`.git` pruned even with `gitignore:false` unless `node_modules` is explicitly present in the requested path or glob, results are capped at 200 by default, and timeout defaults to 5 seconds. Local `find` prefers the bundled Rust native `glob` implementation derived from oh-my-pi, falling back to the packaged `fd` helper only when native bindings are unavailable. Results include `scopePath`, `fileCount`, `files`, truncation/missing-path metadata, and streamed `onUpdate` snapshots during long scans.

`search` searches file contents with a regex across files, directories, globs, archive members, SQLite selectors, and internal URLs. It accepts `pattern`, optional `paths`, `i`, `case`, `gitignore`, and `skip`. Omitted, empty-string, or empty-array `paths` search the workspace root; copied quoted paths are normalized, exact filesystem paths with delimiters are preserved, and comma/semicolon-joined filesystem/resource paths are split when at least one part resolves while whitespace joins require every part to resolve. Whitespace-only patterns are rejected; other patterns are preserved verbatim, including ripgrep-style inline flags such as `(?i)`, `(?m)`, and `(?x)` for resource-backed selectors. Default search output is paged by matching files (20 by default), caps multi-file output to 20 matches per file while single-file searches allow 200 matches, uses `search.contextBefore`/`search.contextAfter` settings (1 before and 3 after by default), and `skip` pages files while single-file searches ignore it. When pagination reaches the internal collection ceiling, the output tells you to refine the pattern/path instead of silently reporting that later matches do not exist. Line selectors scope matches first and then render context around in-range matches, so context-only lines outside the selected range do not count as hits. Local filesystem search prefers the bundled Rust ripgrep-backed native `grep` implementation derived from oh-my-pi, preserving upstream 4 MiB file caps, context, line truncation, hidden-file defaults, and `.gitignore` handling; resource-backed searches use the native in-memory matcher when available and keep the JS fallback for multiline/resource edge cases. Search details include scope, counts, file lists, per-file match counts, missing paths, displayed content metadata, and `fileLimitReached`/`meta.limits.fileLimit` when pagination has more matching files. Hashline search rows preserve match/context markers as `*LINE:...` and ` LINE:...`.

Directory `read` output renders an oh-my-pi-style depth-2 tree sorted by most-recent modification time, includes file sizes/relative ages, prunes `.git`/`node_modules`, and caps child directories to 12 entries with an elision marker while preserving the oldest shown entry. `read`, `write`, and `search` support local zip/jar/tar/tgz/gzip archive members without a Python dependency, including archive members literally named `raw`, `conflicts`, `1`, `L1`, or paths like `raw:notes.txt`, SQLite table/row selectors (`limit`, `offset`, `where`, `order`, `schema`, and `sampleRows` query parameters), `skill://` and source-backed `local://` selectors (which use the underlying filesystem path for mutable hashline labels/snapshots), and session-router-backed internal resources such as `artifact://`, `agent://`, `history://`, `issue://`, `pr://`, `rule://`, `mcp://`, and `vault://` when the host exposes a router. Workspace-scoped selectors (`local://`, built-in `skill://`, local archives, and SQLite selectors) reject lexical and symlink escapes outside the workspace or skill root. Existing non-SQLite `.db`/`.sqlite` files remain plain files; archive writes reject directory targets ending in `/`; archive writes return their resolved archive path, SQLite writes return source-path metadata, shebang writes are chmodded executable and report `madeExecutable`, SQLite table reads show schema plus a 5-row sample by default, SQLite query reads default to 20 rows with a 500 cap (raw `?q=` supports single-statement `SELECT` queries only, rejects `sqlite_%` internals, `pragma_*` table-valued functions, and dangerous keywords such as `ATTACH`, and is capped to 1000 rows via streaming iteration; table lists cap to 500 excluding `sqlite_%` tables, and table row counts probe at most 50,001 rows), table writes accept `{}` as `INSERT DEFAULT VALUES`, row writes parse non-empty JSON5-style objects including comments, and SQLite writes validate column names/scalar values before binding (empty SQLite row writes delete only when a row id is present). `conflict://<id>` and `conflict://*` writes splice conflict marker regions, expand `@ours`, `@theirs`, and `@base`, and return fresh hashline snapshot headers for resolved files; scoped conflict sides such as `conflict://1/ours` are read-only. Plain `write` refuses to overwrite generated-looking files when generated markers appear near the top of the file. `read` extracts readable text for HTML URLs and notebooks (`.ipynb` cells use 0-based `cell:N` IDs and preserve unknown top-level notebook fields), and routes PDFs plus Office/document formats (`.doc`, `.docx`, `.ppt`, `.pptx`, `.xls`, `.xlsx`, `.rtf`, `.epub`) through the same `markit-ai` converter path as oh-my-pi, including upstream unsupported-format messages when no converter is available. Extensionless URL downloads are decoded when the `Content-Type` identifies the document type. Oversized URL/resource/document reads return guidance plus structured details so collapsed renderers still surface the block reason. Successful `read` results consistently return `details.meta.source`/`sourcePath` (plus `truncation`/`limits` when truncation or list limits apply), and `read`/`search` accept line selectors such as `file.ts:5-16`, `file.ts:5+3`, `file.ts:5-16,960-973`, or `https://example.test/page:5-8`; bounded read selectors include one leading and three trailing context lines unless `:raw` is used for unformatted content, and out-of-range selectors report a clear beyond-EOF message instead of returning an empty success.

Plain URL reads follow oh-my-pi's fetch-pipeline truncation contract: unselected URL output shows the first 300 rendered lines (capped at 50 KiB), preserves full-output artifact/truncation metadata when available, and does not hard-block solely because the rendered URL body is large. By default Atomic rejects private, localhost, cloud-metadata, numeric/short-form private-IP URL targets (for example `2130706433`, octal/hex dotted forms, and `127.1`), IPv4-compatible and IPv4-mapped IPv6, NAT64, 6to4 private-address forms, and the full IPv6 link-local `fe80::/10` range, revalidates each manual redirect, pins DNS-validated addresses for outbound fetches, and caps streamed URL bodies; `ATOMIC_ALLOW_PRIVATE_URL_READS=1` is a dev-only escape hatch for trusted local tests and must not be set from untrusted project configuration. Local text reads use the shared 3,000-line/50 KiB output cap, while search match/context lines use the upstream 512-character cap before emitting a truncation notice.

## Persisted tool output

Output that does not fit in a tool result is written to a file, and the result points at it — `Full output: <path>` for `bash`, `Full output saved to: <path>` for any tool result that crosses the persistence threshold. Those files are storage, so Atomic bounds where they go, how large they get, and how long they live.

**Where.** A session that persists to disk keeps its tool results in `<sessionDir>/tool-results/`, unchanged. Everything else — `bash` overflow logs, streamed spill files, and tool results for in-memory sessions — goes under one owner- and session-scoped temp tree:

```text theme={null}
<tmpdir>/atomic-<uid>/<session-id>/
```

On Windows, where there is no uid, the account name is used instead — qualified by its domain and followed by a short digest, so that `CONTOSO\Alice` and `FABRIKAM\Alice`, or two names that reduce to the same safe path component, do not share one tree. The session id is reduced to a single safe path component, so a session id containing separators or `..` cannot place files outside that tree.

Directories are created with `0700` and files with `0600` on platforms that support them. On POSIX that is enforced: a root owned by another account is refused and a too-permissive one is tightened. On Windows, Node exposes neither an owner SID nor a POSIX mode, so before adopting an existing component Atomic reads its security descriptor through PowerShell `Get-Acl` and decides from SIDs: the owner must be the current user, SYSTEM, or Administrators, and every access-allowed DACL entry must grant only those principals. A foreign-owned root, a root other accounts can access (a machine-wide redirected `TMP`), and a descriptor that cannot be read or fully parsed are all refused. A successful verification is cached per path per process, keyed to the directory's metadata identity — creation, change, and last-write times — so both a directory swapped underneath the path and an in-place DACL edit (which bumps the NTFS change time) force a fresh descriptor read; only a directory whose metadata is unchanged skips the subprocess.

Because that path is predictable, Atomic validates it rather than trusting it. Every component below the system temp directory is created one level at a time and checked: a symlink is refused outright, a directory owned by another account is refused, and a directory of this account's left too permissive is tightened to `0700` and re-checked. Refusal fails closed — the tool runs with no spill file and reports no path, instead of writing your command output into a directory someone else planted. Only the final session directory is treated as replaceable: a stale file or link sitting exactly where a session directory belongs is removed (a link by itself, never its target) and recreated.

**How large.** No single persisted-output file exceeds 64 MB. Output that lands exactly on the cap is kept whole; only output that passes it is cut off there, with `[Output truncated: persisted-output cap of 67108864 bytes reached]` appended in place of the rest. The cut lands on a character boundary even when a multi-byte character is split across two chunks of streamed output, and bytes that are not valid UTF-8 — binary command output — are written through unchanged rather than decoded. The truncation is in the file only; the tool result and its `fullOutputPath` are unaffected. If a spill file cannot be written at all, `bash` reports no path rather than one pointing at a file that is not there.

**How long.** Tool-output storage is reaped on age. A sweep runs shortly after startup, off the startup path, and removes session temp trees and `tool-results` directories whose newest file is more than 30 days old. It covers the default project-nested session roots and a custom session directory chosen with `--session-dir`, `ATOMIC_CODING_AGENT_SESSION_DIR`, or the `sessionDir` setting. The newest entry decides the whole directory: one fresh file keeps everything beside it, so a path recorded weeks ago stays readable as long as the tree is still in use. Each target is throttled to once a day by a `.last-cleanup` marker and guarded by a `.cleanup.lock` exclusive lock, so concurrent Atomic sessions never race each other; for session storage that pair lives in a control directory under the temp root rather than inside the scanned directory. A tree belonging to a session the running process is using is never reaped, so a `Full output saved to:` path stays valid for the life of the session that produced it. Session transcripts and `.jsonl` session files are outside the sweep entirely: only the temp trees and `tool-results` directories are ever deleted, symlinks are never followed out of a target, and a directory the sweep cannot verify as a real directory is skipped rather than read.
