> ## Documentation Index
> Fetch the complete documentation index at: https://docs.nanoclaw.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Dial

> Give NanoClaw a real phone number via Dial — SMS and AI-handled voice calls, email sign-in, 6-digit SMS pairing, owner-only or public lines, and the optional in-sandbox dial CLI tool.

The Dial adapter gives your agent a **real phone number** through [Dial](https://getdial.ai): people text or call it, and the agent answers over SMS; the agent can also place outbound calls that Dial's AI voice agent handles. It's a **native adapter** — no Chat SDK bridge — and both directions go through the `dial` CLI: outbound SMS is `dial message --json`, inbound events arrive through the `dial listen` daemon, which runs a small handler script that spools each event into `data/dial/inbound/` for the adapter to pick up. No public URL, webhook, or open port.

One number is a **shared line**: the number itself is the messaging group, and every person who texts it gets their own thread (and so their own session). Because a phone number is guessable and the agent holds an account-scoped Dial credential, each line carries an explicit access policy — **owner-only** (the default) or **public**.

## Prerequisites

* A working NanoClaw install ([quickstart](/quickstart)) with Node available to the service (the `dial` CLI is a Node script)
* An email address you can read — Dial signs you in with a one-time code; the wizard creates the account if you don't have one (you accept Dial's [Terms](https://getdial.ai/terms) and [Privacy Policy](https://getdial.ai/privacy) by continuing)
* A phone to pair from — the first phone that completes pairing becomes the install's owner
* A user-service supervisor (launchd on macOS, `systemd --user` on Linux) for the `dial listen` daemon; without one, outbound works but inbound doesn't start on its own

## Install

Dial is offered in the first-run setup wizard (**Yes, connect Dial**), or add it later by running `/add-dial` in Claude Code. The wizard flow:

<Steps>
  <Step title="Install the CLI and pin its path">
    The wizard installs the `dial` CLI if it's missing (`curl -fsSL https://getdial.ai/install | bash`) and writes its **absolute** path to `.env` as `DIAL_CLI_PATH`. The adapter runs inside the background service, which doesn't inherit your shell's `PATH`; a bare `dial` there fails with `ENOENT` and the channel comes up connected but deaf.
  </Step>

  <Step title="Sign in with your email">
    If the host is already signed in, the wizard offers to reuse that account or switch. Otherwise it asks for your email, Dial sends a 6-digit code, and you paste it. Verification provisions your number and installs Dial's `nanoclaw` agent profile. The wizard then sets the number's inbound voice instruction (a friendly receptionist that takes a message) and pins that number as the CLI's default sender — on multi-number accounts the CLI's saved default is the *oldest* number, which would otherwise send from a line nobody is listening on.
  </Step>

  <Step title="Choose who may text the line">
    `owner` admits only the phone you pair next; everyone else is refused — including people your agent calls, so they can't text back. `public` lets anyone who knows the number start a conversation, each in their own thread. The answer is written to the line's `unknown_sender_policy` when the line is registered and can be changed later with `ncl messaging-groups update --id <id> --unknown-sender-policy public|strict`.
  </Step>

  <Step title="Restart and register the line">
    The service restarts so the adapter is live and polling, `dial listen install` starts the event daemon, `dial local-target add cmd …/data/dial/handle-dial-event.sh` registers NanoClaw as the command target, and the line is created as a group (`ncl messaging-groups create --channel-type dial --platform-id <E.164> --is-group 1 …`). Re-runs return the existing row and never reset a policy you changed.
  </Step>

  <Step title="Pair your phone">
    The terminal shows a one-time **6-digit code** and a scannable QR (an `SMSTO:` link that opens Messages pre-filled). Text exactly those 6 digits to your Dial line. The live adapter matches the code, replies "Paired ✅", and records your number; the wizard — never an inbound SMS — then grants you the `owner` role (at most one owner per install) and wires the line to your first agent. The step waits 5 minutes; codes expire after 10.
  </Step>

  <Step title="Add phone superpowers (optional)">
    The wizard offers to run `/add-dial-tool`, which puts the `dial` CLI inside your agents so they can text and call from any channel. `/add-dial` asks which agents may use it and hands the answer down, so you answer in your own terminal rather than into a nested step. On a first install there are no agent groups yet, so the question is skipped and the tool installs for every agent. Re-run `/add-dial-tool` any time to narrow that down. See [The Dial tool](#the-dial-tool-sms-and-calls-from-inside-the-sandbox) below. Saying no keeps the channel working without it.
  </Step>
</Steps>

Right after pairing the wizard tells you what the line can do: **calls both ways and inbound texts work immediately**. Outbound SMS to **US** numbers needs 10DLC registration on the Dial side (\$25, usually 3–5 business days, from the [Dial dashboard](https://getdial.ai/dashboard/numbers)); until then US carriers drop those texts and the adapter reports each one as a delivery failure (below). Non-US recipients are unaffected.

To wire more lines or move the line between agents later, run `/manage-channels`. To add a **second number** to the same install, run `/add-dial-number` — each number is its own line with its own access policy, wired to the same agent or a different one, and replies always go out from the number the person texted.

## Platform notes

* **Identifiers** — the line's platform id is the bare E.164 number (`+14155550123`, no prefix); senders are `dial:<E.164>` and each correspondent is a thread keyed by their number. The adapter sets `supportsThreads: true` and creates lines as groups with `threads: true`, so `per-thread` isolation is what keeps correspondents apart — see the [entity model](/concepts/entity-model).
* **Text only** — the adapter sends the text of a reply; attachments are not delivered over SMS. Long replies are split into 1,500-character chunks at paragraph breaks. A reply with no thread (no correspondent to send to) is dropped with a warning.
* **Delivery verdicts** — `dial message` returns as soon as Dial accepts the text; the carrier's verdict arrives later as a `message.status_changed` event. The adapter tracks every send (persisted in `data/dial/pending-sends.json`, so a restart can't turn a bounce into silence), reconciles anything without a verdict after 3 minutes by reading back `dial message list` once a minute, and gives up tracking after 24 hours. An `undelivered`/`failed` verdict is logged as an error and routed into the same thread as a `[NanoClaw system notice: …]` telling the agent the person did not get the message, not to reply, and not to resend over SMS — at most one notice per correspondent per hour, so a bouncing conversation can't loop.
* **Voice calls** — when a call ends the agent gets a `[Voice call inbound|outbound … ended, Ns.]` notice; an inbound call's notice goes to the caller's thread, an outbound call's to the **owner's** thread (the person who asked for the call). Shortly after, `call.transcribed` delivers the transcript inline (clipped at 4,000 characters; `dial call get <id>` has the rest). Inbound calls are answered by Dial's AI voice agent using the number's inbound instruction, which you change with `dial number set <number> --inbound-instruction "…"`.
* **Pairing guard** — codes expire after 10 minutes; five wrong codes texted to a line inside 10 minutes lock that line for 15 minutes, during which even the right code is refused and nothing is texted back. Tune with `DIAL_PAIRING_TTL_MS`, `DIAL_PAIRING_MAX_ATTEMPTS`, `DIAL_PAIRING_ATTEMPT_WINDOW_MS`, `DIAL_PAIRING_COOLDOWN_MS`. Pending codes live in `data/dial-pairings.json`.
* **Credentials** — the adapter never sees a key. The CLI authenticates from the auth file it wrote at sign-in (`~/.local/share/dial/auth.v1.json`, or under `$XDG_DATA_HOME`); the adapter only reads it to learn the account's number and to confirm the host is signed in — no auth file, and the channel is skipped at startup. Every `dial` call the setup makes carries `DIAL_USER_AGENT=nanoclaw/<version>` so the account's requests are attributable to this install in Dial's logs.
* **Forged notices** — inbound text and transcripts that contain the literal `NanoClaw system notice:` prefix are rewritten to `(sender-typed) NanoClaw system notice:` before an agent sees them, so a stranger can't impersonate the adapter.

## The Dial tool: SMS and calls from inside the sandbox

`/add-dial-tool` is independent of the channel — you can install it alone, or let `/add-dial` offer it. It gives chosen agents the `dial` CLI as a **container tool**:

* Pins `@getdial/cli` (0.37.0) in `container/cli-tools.json` and rebuilds the image, and mounts a sandbox-aware `dial-cli` skill so the agent knows the CLI runs keyless in there.
* Stores the account's API key in the [OneCLI vault](/operate/credentials) for `api.getdial.ai`, so requests from the container are authenticated at the gateway — the key never enters the sandbox.
* Asks **which agents may use Dial** and enforces the answer with one OneCLI policy rule, `Dial: blocked agents`, that blocks `api.getdial.ai` for every agent you did not choose (`all` means no rule). Answer `all`, `none`, or a list of `ag-…` ids separated by commas with no spaces. If you run it from `/add-dial`, the parent asks this question instead and passes your answer down. An agent you left out sees `403 blocked_by_policy`; a `401` means the vault secret needs refreshing. Agents created *after* the run have Dial until you re-run the skill, which is also how you change the list.
* Replaces the OneCLI secret on every run rather than updating it. `onecli secrets update` accepts a new value only on the command line, and the key must never sit there. The skill deletes the stale secret and creates a fresh one from a temporary file, then merges the new secret id into the chosen selective-mode agents further down, so re-onboarding and key rotation both work.
* Needs OneCLI gateway 1.42. Before anything is written it reads the gateway version and stops, with a message naming 1.42.0, if the gateway is older or newer. The block rule is published before the Dial key is stored. Gateways on 1.43 or later are not supported by this skill yet.

<Warning>
  An agent with the tool can text and call **any** number and buy more numbers, billed to your Dial account. Grant it deliberately, and remember that on a `public` line every stranger who reaches that agent gets a turn with it.
</Warning>

When you install the tool, `/add-dial` also appends the wired line to the container skill so agents always pass `--from-number <your line>` — inside the sandbox there is no auth file, so the CLI has no default sender.

## Troubleshooting

* **Channel is connected but nothing comes in** — the command target isn't registered or the daemon isn't running. `dial doctor --json` should show `listen.running: true`; `dial local-target list` should list `…/data/dial/handle-dial-event.sh`. Fix with `dial listen install` (needs launchd / `systemd --user`) and make sure `DIAL_CLI_PATH` in `.env` is absolute and the service can resolve `node`.
* **Pairing never completes** — the adapter must be running to observe the code, and the text must be *only* the 6 digits. If it timed out (5 min) or the code went stale (10 min), re-run the step for a fresh one.
* **"Pairing is paused for about N min"** — the brute-force lock; wait it out, then re-run for a new code.
* **Texts to US numbers never arrive** — 10DLC registration is pending. The agent receives a delivery-failure notice per correspondent; register the number in the Dial dashboard.
* **Replies come from the wrong number** (multi-number accounts) — the installed adapter predates multi-line routing. `/add-dial-number` detects this and refreshes `dial.ts` (with its user-agent helper) from the `channels` branch before continuing.
* **A tool-enabled agent gets `403 blocked_by_policy`** — it wasn't chosen; re-run `/add-dial-tool`. `dial: command not found` inside a container means the image predates the manifest entry: `./container/build.sh`, then `ncl groups restart --id <group-id>`.
* **`/add-dial-tool` ends with `Applied with gaps`, and every agent still runs the old image.** An earlier step in the run failed, so the closing group restart was skipped. That gate is deliberate. Fix the reported failure, then re-run the skill.
* **Everything green but no replies** — `pnpm exec vitest run src/channels/dial-registration.test.ts`; red means the barrel import drifted, so re-run `/add-dial`.

For service-level checks (logs, restarts, wiring queries), see [troubleshooting](/operate/troubleshooting).


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.