Installing Agentic AI System, WhatsApp and a fresh Dev Environment

Installing Agentic AI System, WhatsApp and Dev Environment on a new Mac

Installing Agentic AI System, WhatsApp and a fresh Dev Environment
Photo by Anton Be / Unsplash

Twilio and WhatsApp

In order to provide more interactive communication between agents and customers, I progressed with WhatsApp, a globally popular, free instant messaging and Voice over IP (VoIP) service owned by Meta.
Before WhatsApp business account can be used, a new phone number is required, I choose Twilio to provide number with WhatsApp Api integration.

It's not instant. Twilio regulatory bundle for InAgentic was approved within 24 hours. It is the compliance step required to use UK phone numbers for sending SMS or WhatsApp messages.

What is it

Twilio requires businesses using UK numbers to submit a regulatory bundle — essentially proof of identity and business registration — before activating numbers for messaging.

What next

One action required: assign the approved bundle to your phone number(s) in the Twilio console. Until you do this, the number won't be fully activated for messaging. The process is:

  1. Go to the Twilio console → Phone Numbers → Manage → Active Numbers
  2. Click on your number
  3. Under the Regulatory Compliance section, select your approved bundle (BU07e2a1a3f1e454d3bb8d0533183d5e48)
  4. Save

While we wait for META WhatsApp setup
A new MacBook Pro 2026 with M5 chip provides better performance for AI development.
While waiting let's build a field guide to bootstrapping a new machine for a multi-service AI stack — Claude Code, AWS IAM Identity Center SSO (and the region gotcha that broke it), Docker Desktop, per-service .env files, and a Twilio WhatsApp integration from sandbox to production.

Introduction

Setting up a new Mac for a multi-service AI stack is rarely just "npm install and go." Between AI-assisted coding, cloud SSO, containerisation, and a messaging integration, there are half a dozen places where a small misconfiguration costs an afternoon. This is the log of doing exactly that setup end to end — Claude and Claude Code, AWS IAM Identity Center SSO, Docker, per-service environment files, and a Twilio WhatsApp integration — including the mistakes that turned up along the way.

By the end you will have:

  • Claude Code — an AI coding agent wired into the project's actual repo and conventions
  • AWS SSO — a working aws sso login flow against IAM Identity Center
  • Docker Desktop — installed and running for local containers
  • Per-service .env files — populated correctly across a polyglot (Node.js + Python) stack
  • Twilio WhatsApp — a sandbox-to-production path for sending and receiving WhatsApp messages

Part 1: Claude and Claude Code

Claude Code is the CLI agent that did the actual setup work described in this article — installing packages, editing config files, and reading through source to figure out exactly which environment variables each service expected. Two things made it effective here rather than just fast:

A CLAUDE.md file at the repo root

Claude Code automatically reads a CLAUDE.md file in the project root before doing anything else. This is where we encode the rules that don't belong in comments — service boundaries, which layer owns which responsibility (Python for AI logic, Node.js for business logic, Next.js for UI only), database conventions (every table gets idtenant_idcreated_atupdated_at), and the hard constraint that AI output is always draft-only and never auto-submits anything irreversible.

Without this file, an agent will happily invent its own conventions. With it, every suggestion gets checked against rules the team already agreed on.

Installing Claude Code

npm install -g @anthropic-ai/claude-code
claude --version

Run claude from inside the project directory and it picks up CLAUDE.md automatically. From there it can read files, run shell commands, edit code, and — with permission — install software and manage cloud credentials, which is exactly what the rest of this setup needed.


Part 2: AWS IAM Identity Center SSO

The project uses AWS SSO (IAM Identity Center) rather than long-lived access keys for developer access — better audit trail, no keys to rotate or leak. The theory is simple: aws configure sso, then aws sso login. In practice, this setup hit a wall that's worth documenting because the error message doesn't point at the real cause.

Step 1: Install the AWS CLI

brew install awscli
aws --version

Step 2: Configure the profile

A named profile keeps this project's credentials separate from anything else on the machine:

aws configure sso --profile your-project-dev
# SSO start URL:  https://your-org.awsapps.com/start
# SSO region:     <see below — this is the trap>
# Account:        <your account ID>
# Role:           <your permission set, e.g. AdministratorAccess>
# Default region: <your workload region>

⚠️ The SSO region is not necessarily your workload region

The internal setup notes for this project listed the SSO region as the same region the app workloads run in. Logging in failed twice with an opaque InvalidRequestException: invalid_request — first on RegisterClient, then (after fixing the config format) on StartDeviceAuthorization. Neither error mentions "region." The actual cause: the IAM Identity Center portal for this AWS Organization is hosted in a different region than the App Runner / Bedrock workloads. The fix was to check where the portal itself lives, independent of where anything is deployed.

How to find your real SSO region

Curl the SSO start URL and look at the redirect headers — the portal's actual region shows up in the CloudFront response, specifically in a link: rel=preconnect header pointing at portal.sso.<region>.amazonaws.com:

curl -sI https://your-org.awsapps.com/start | grep -i "link|location"

Whatever region appears there is the sso_region value that belongs in your profile — not the region your resources happen to run in.

Step 3: Log in

aws sso login --profile your-project-dev

This opens a browser (or prints a device code and URL for headless/remote sessions) — approve the request, and the CLI caches a short-lived session token locally.

Step 4: Find your role name if you don't already know it

If the permission set name isn't documented anywhere, list it after logging in via an SSO session:

aws sso list-account-roles --account-id <your-account-id> --region <sso-region>   --access-token "$(jq -r .accessToken ~/.aws/sso/cache/*.json | head -1)"

Step 5: Verify

aws sts get-caller-identity --profile your-project-dev

A successful response returns your account ID, user ARN, and assumed role — confirming the whole chain (start URL → SSO region → account → role) is wired correctly. Re-run aws sso login --profile your-project-dev whenever the cached session expires; there's no need to reconfigure anything.


Part 3: Docker Desktop

Docker wasn't installed on this machine at all — no daemon, no CLI. The install itself is a one-liner, but it has one interactive step that's easy to trip over when running through an automated shell:

brew install --cask docker

Homebrew needs sudo partway through this cask install, to symlink the docker binary and set up a privileged helper. That prompt needs an interactive terminal with a real TTY — it fails silently (and rolls back the whole install) inside any non-interactive shell, including automation tools. Run this one from a terminal you can type a password into.

After the cask installs, the CLI is on your PATH but the daemon isn't running yet — Docker Desktop is a real application that needs to be launched:

open -a Docker
# wait for the whale icon in the menu bar, then:
docker info

First launch asks for a few macOS permissions (network filtering, Rosetta on Apple Silicon if you'll run x86 images). Once docker info returns a Server block instead of a connection error, you're ready to build and run containers.


Part 4: Environment Variables Across a Polyglot Stack

A project with a Next.js frontend, several Node.js services, and a Python AI service ends up with one .env.example per service — and it's easy to lose track of which variable belongs where. Before copying anything into place:

Confirm the gitignore is doing its job

grep -i env .gitignore
# .env*
# .env
git status --porcelain | grep -i env   # should print nothing once files exist

Match keys to each service's own template

Rather than guessing, read every .env.example in the repo and fill in exactly those keys per service — no more, no less:

find . -maxdepth 2 -iname ".env.example" -not -path "*/node_modules/*"
# ./.env.example                 (Next.js app)
# ./ai-service/.env.example      (Python FastAPI — AWS Bedrock, LangSmith)
# ./mcp-server/.env.example      (MCP tool server)
# ./enrolment-agent/.env.example (LangGraph worker)
# ...

A few things worth calling out from doing this across six services in one pass:

  • Some services intentionally have no AWS access keys in their .env — comments like "AWS credentials are resolved from the environment / IAM role" mean the service expects aws sso login or an instance role, not static keys. Adding keys there would be redundant and one more secret to rotate.
  • Two services that both call the same internal API sometimes use different API key values for it — don't assume a shared secret name means a shared secret value. Check each source file, not just the variable name.
  • DATABASE_URL that isn't explicitly given anywhere often just needs assembling from the individual DB_HOST / DB_PORT / DB_NAME / DB_USER / DB_PASSWORD values used elsewhere in the stack — construct it as a standard postgresql://user:password@host:port/dbname?sslmode=require URI.

Handle the secrets file itself with care

Wherever the raw secrets originate — a password manager export, a handoff doc, a plaintext file from a previous machine — treat that source file as more sensitive than any single .env, because it's every credential in one place. Once the individual .env files are populated and verified, delete or securely archive the combined file rather than leaving it sitting on disk outside version control's protection.


Part 5: Twilio and WhatsApp

The last piece is an outbound/inbound WhatsApp channel via Twilio — useful for deadline reminders, filing confirmations, or two-way support, without building a WhatsApp Business API integration from scratch.

Step 1: Create a Twilio account and get credentials

  1. Sign up at twilio.com and verify your email and phone number.
  2. From the Twilio Console dashboard, copy your Account SID and Auth Token.
  3. Store both as environment variables — never hardcode them: TWILIO_ACCOUNT_SIDTWILIO_AUTH_TOKEN.

Step 2: Start in the WhatsApp Sandbox (development)

Twilio provides a shared sandbox number for development so you don't need Meta's approval just to start building:

  1. In the Console, go to Messaging → Try it out → Send a WhatsApp message.
  2. From your own WhatsApp, send the join <your-sandbox-code> message to the sandbox number shown — this opts your number in for 72 hours (re-join any time it expires during development).
  3. Note the sandbox number — it's used in the whatsapp:+14155238886 format for the "from" address in the API.

Step 3: Send a test message

npm install twilio
const twilio = require('twilio');
const client = twilio(process.env.TWILIO_ACCOUNT_SID, process.env.TWILIO_AUTH_TOKEN);

await client.messages.create({
  from: 'whatsapp:+14155238886',
  to: 'whatsapp:+',
  body: 'Test message from the sandbox.',
});

Step 4: Configure the inbound webhook

To receive replies (or inbound support messages), point Twilio at an endpoint on your own backend:

  1. In the Console, under the WhatsApp sender's settings, set "When a message comes in" to a URL like https://your-api.example.com/webhooks/twilio-whatsapp.
  2. On that route, validate the X-Twilio-Signature header using Twilio's request validator — never trust an unsigned webhook body.
  3. Keep this endpoint in the Node.js backend, alongside the other business-logic webhooks — Twilio message handling is business logic, not AI logic, even if an AI service later drafts the reply text.

Step 5: Go to production — a real WhatsApp sender

The sandbox is for development only and can't message people who haven't opted in with a join code. Production requires:

  • Meta Business Manager account, with your business verified.
  • A dedicated WhatsApp Sender requested through Twilio, tied to a phone number, with your business display name submitted for Meta's approval (typically a few business days).
  • Message templates pre-approved by Meta for any message you send outside a 24-hour customer-initiated conversation window — free-form replies only work inside that window.

Where this fits the rest of the stack

If message content is ever AI-generated — a personalised deadline reminder, a drafted support reply — that generation belongs in the Python AI service, returned as a draft with an explanation, and a human (or an explicit, already-approved rule) decides whether it actually sends. The Node.js backend owns the Twilio API call itself, the webhook signature check, and the decision of when to send — Twilio is a delivery mechanism, not a place to embed business or AI logic.


Quick Reference

# AWS SSO
aws sso login --profile your-project-dev
aws sts get-caller-identity --profile your-project-dev

# Docker
open -a Docker
docker info

# Env files — verify nothing leaked into git
git status --porcelain | grep -i env

# Twilio WhatsApp sandbox test
# (from your phone) join <sandbox-code>  →  to the Twilio sandbox number
node send-test-message.js

Frequently Asked Questions

Why did AWS SSO fail with "invalid_request" and no other detail?

In this case it was an sso_region mismatch — the CLI was pointed at the region where workloads run rather than the region hosting the actual IAM Identity Center portal. Confirm the portal's real region from the CloudFront redirect headers on the SSO start URL before assuming the config format is wrong.

Why did the Docker cask install roll back with a sudo error?

Homebrew's cask install for Docker Desktop needs an interactive sudo prompt for a privileged helper step. Run it from a terminal with a real TTY, not through a non-interactive automation shell.

Do I need static AWS keys if I'm already using SSO?

No — services that read AWS credentials "from the environment or IAM role" are designed to pick up the active SSO session or an instance role automatically. Adding static keys on top is redundant and creates one more secret to manage and rotate.

Can I send WhatsApp messages to anyone once the sandbox works?

No. The sandbox only reaches numbers that sent the join code, and that opt-in expires after 72 hours. Reaching arbitrary numbers requires a production WhatsApp Sender approved through Meta Business Manager.

Where should AI-drafted WhatsApp messages be reviewed before sending?

Keep the draft-only boundary the same as everywhere else in the stack: the AI service proposes content with an explanation, and either a human or an already-approved business rule in the Node.js layer decides to actually call the Twilio send API. The AI layer should never hold the credentials to send a message itself.

✓  TL;DR
What we learned setting up the new Mac and WhatsApp channel
A CLAUDE.md file prevents Claude Code from inventing its own conventions. Encoding service boundaries, which layer owns which responsibility, and the hard rule that AI output is always draft-only means every suggestion gets checked against rules the team already agreed on, rather than assumed fresh each session.
AWS SSO failures pointed at the wrong cause entirely. An opaque invalid_request error had nothing to do with config format — the IAM Identity Center portal was hosted in a different region than the actual workloads, discoverable only by checking the CloudFront redirect headers on the SSO start URL, not the region resources happen to run in.
Docker's install needs a real terminal, not automation. The Homebrew cask requires an interactive sudo prompt for a privileged helper step — it fails silently and rolls back inside any non-interactive shell, including automation tools.
Matching keys is safer than guessing across a polyglot stack. Reading each service's own .env.example individually surfaced real gotchas: some services deliberately have no static AWS keys (they resolve credentials from SSO or an IAM role), and two services calling the same internal API sometimes used different key values for it despite matching variable names.
The combined secrets source is more dangerous than any individual .env. Once individual files are populated and verified, the original handoff document or password manager export — every credential in one place — gets deleted or securely archived rather than left sitting on disk.
WhatsApp production access is gated by Meta, not just Twilio. The sandbox only reaches numbers that opted in with a join code, and that opt-in expires after 72 hours — reaching arbitrary numbers requires a Meta-approved WhatsApp Sender and pre-approved message templates for anything outside a 24-hour conversation window.
The draft-only boundary applies to messaging channels too. AI-generated WhatsApp content is proposed by the Python AI service with an explanation; the Node.js backend owns the actual Twilio send call, the webhook signature check, and the decision of when to send — the AI layer never holds credentials to send a message itself.
Coming in future chapters
Putting the new toolset to work — Claude Code, AWS SSO, Docker, and the WhatsApp channel wired into the rest of the pipeline.

Machine bootstrapped, SSO working, containers running, and a WhatsApp channel ready to wire into the rest of the pipeline.

💡
Tomorrow we'll start using our new toolset