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) with Node available to the service (the
dialCLI 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 and Privacy Policy 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 --useron Linux) for thedial listendaemon; 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:
1
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.2
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.3
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.4
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.5
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.6
Add phone superpowers (optional)
The wizard offers to run
/add-dial-tool, which puts the dial CLI inside the agents you choose so they can text and call from any channel. See The Dial tool below. Saying no keeps the channel working without it./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 aredial:<E.164>and each correspondent is a thread keyed by their number. The adapter setssupportsThreads: trueand creates lines as groups withthreads: true, soper-threadisolation is what keeps correspondents apart — see the 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 messagereturns as soon as Dial accepts the text; the carrier’s verdict arrives later as amessage.status_changedevent. The adapter tracks every send (persisted indata/dial/pending-sends.json, so a restart can’t turn a bounce into silence), reconciles anything without a verdict after 3 minutes by reading backdial message listonce a minute, and gives up tracking after 24 hours. Anundelivered/failedverdict 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.transcribeddelivers 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 withdial 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 indata/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. Everydialcall the setup makes carriesDIAL_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) incontainer/cli-tools.jsonand rebuilds the image, and mounts a sandbox-awaredial-cliskill so the agent knows the CLI runs keyless in there. - Stores the account’s API key in the OneCLI vault 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 (
all,none, or a list ofag-…ids) and enforces it with a per-agent OneCLI block rule. An agent you left out sees403 blocked_by_policy; a401means 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.
/add-dial, the skill 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 --jsonshould showlisten.running: true;dial local-target listshould list…/data/dial/handle-dial-event.sh. Fix withdial listen install(needs launchd /systemd --user) and make sureDIAL_CLI_PATHin.envis absolute and the service can resolvenode. - 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-numberdetects this and refreshesdial.ts(with its user-agent helper) from thechannelsbranch before continuing. - A tool-enabled agent gets
403 blocked_by_policy— it wasn’t chosen; re-run/add-dial-tool.dial: command not foundinside a container means the image predates the manifest entry:./container/build.sh, thenncl groups restart --id <group-id>. - 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.