TerminalVibes / Talking to the Network
Part 9

Talking to the Network

"The server is running." — prove it.

In Processes & Ports you took a port back and started a server on it. Now let's talk to it. This part is about asking questions over the network and reading the answers: what localhost actually means, how to check a server yourself instead of trusting a claim, how to pull one value out of a wall of JSON — and how to handle the API keys that make all of it work without leaving them lying around in your shell history.

Important
The habit this part builds is the one Terminal for the AI Era will formalise, aimed here at a new target: verify with your own command. When an agent says the server is up, the deploy worked, or the API returned success, there is a one-line way to check — and now you'll know it.

9.1 What Is localhost?

localhost is the name your machine uses for itself. Ask for localhost and the request never touches the network — it leaves one program on your computer and goes straight into another. That's why a dev server announcing "listening on http://localhost:3000" is visible to you and to nobody else. A URL packs several answers into one line:

localhost = this very machine · the port is which door
Reading a URL
http://localhost:3000/api/health
└─┬─┘  └───┬───┘ └─┬┘ └────┬────┘
                        └── which room inside  the path
                 └── which door  the port
          └── which machine  here, this one
  └── the language  http

The first part names the language: HTTP, the hypertext transfer protocol — the rules a program and a server use to ask and answer. On the public web you'll see https instead, the same conversation encrypted so nobody in between can read it. And the tail end is a URL path, not a folder on your disk: /api/health exists only because someone wrote that route into the server. Ask for one nobody wrote and you get a 404 — the server saying it has no such path — not a broken machine.

You've met ports already: they're the numbered doors from Who's on Port 3000?, and the same rule applies — one program per port. That's why the numbers recur. 3000 is the Node convention, 5173 is Vite's — the tool that runs the dev server for a great many JavaScript projects — and 8080 is the classic alternate web port. If your agent starts a project and mentions a number in that neighborhood, it's telling you which door to knock on.

Tip
127.0.0.1 is the same place as localhost — the numeric version of "here." Machines find each other on a network by IP address — internet protocol, the numbering scheme every networked machine answers to — and 127.0.0.1 is the one number reserved to mean this machine. You'll see both spellings in error messages and config files; they mean the same computer.
Vibe it

"My app says it is running on localhost:5173 but the browser shows nothing — how do I check from the terminal?"

"Explain the difference between localhost and a real domain like example.com"

9.2 curl — Ask the Network

curl sends a request and prints the reply. That's it — and that's enough to settle most arguments with an agent. Instead of trusting "the server is running," you ask the server:

Send a request. Read the reply. Believe nothing else.
Asking, and saving the answer
curl localhost:3000/health
{"status":"ok"}

curl localhost:3000/health          # with nothing listening
curl: (7) Failed to connect to localhost port 3000: Connection refused

curl -s -o status.json localhost:3000/health   # save it instead of printing
curl -I localhost:3000/health                  # just the headers

Four flags cover almost everything you'll need. -o FILE saves the reply instead of printing it. -s ("silent") hides the progress meter — always use it inside pipelines and scripts. -I shows you just the headers a server sends back, which is the quickest "is this thing alive and what does it serve?" And -H adds one to what you send.

A header is a name/value line travelling alongside the request, carrying everything that isn't the content — what format you want back, who you are, what software is asking. (Nothing to do with the header row of a spreadsheet; the word just gets reused.) An API — an application programming interface, a door a service opens for other programs rather than for people — knows which requests are yours because your key rides in one. That's the whole subject of Keys & Secrets.

Caution
Now you can read the red-flag pattern. You will meet curl ... | bash everywhere, and it decodes with what you already have: curl fetches a script off the internet, and the pipe feeds it straight into a shell that runs it — unread, unreviewed, with your permissions. The two-step version is always available: curl -o install.sh <url>, read the file, then run it. When it's the right call to skip that step — and it sometimes is — is the subject of Read Before You Run.
Vibe it

"Check whether my local API is responding on port 8000 and show me the raw response"

"You said the deploy succeeded — give me a curl command that proves the new version is live"

Try It: Is It Alive?

The agent says the server is up. Ask it yourself with curl, then save the reply to status.json with -s -o so you have the receipt.
Is It Alive?

Loading playground...

9.3 Reading JSON with jq

APIs answer in JSON — labelled boxes inside labelled boxes, built out of nested { "name": value } pairs. Printed raw it's a wall of braces; what you usually want is one value out of the middle. jq is the tool that reaches in and takes it, and since it reads from a pipe, it joins the pipelines you built in Text & Pipes.

One thing the sandbox hides: jq is not standard equipment. It's a third-party tool you install first (Package Managers), so your first | jq on a fresh machine may well answer command not found rather than a version number.

APIs answer in nested boxes — jq holds the keys
From wall of braces to one value
curl -s api.vibecloud.dev/releases
{"latest":"2.1.0","downloads":48213,"items":[{"tag":"2.1.0"}]}

curl -s api.vibecloud.dev/releases | jq .latest
"2.1.0"

curl -s api.vibecloud.dev/releases | jq -r .latest    # -r = raw, no quotes
2.1.0

curl -s api.vibecloud.dev/releases | jq -r .items[0].tag
2.1.0

A filter is just a path: . is the whole document, .latest is one key, .server.port walks deeper, and .items[0] takes the first element of a list. The -r flag matters more than it looks: without it a string keeps its JSON quotes, which breaks the next command in the pipeline. Reach for -r whenever the value is going somewhere else.

Tip
If jq says parse error, look at the raw reply. It means one thing only: jq tried to read what arrived as JSON, and the bytes weren't JSON. Nine times out of ten what arrived is an HTML error page — the tag-based format web pages are written in, so you'll see angle brackets, a leading <!doctype html> and no braces anywhere. The real problem is upstream: a wrong URL, or a dead server. Drop the | jq and read what actually arrived, the same build-the-pipeline-one-stage-at-a-time habit from Text & Pipes.
Vibe it

"Pull just the version number out of this API response and explain the jq filter you used"

"This jq command returns null and I do not know why — help me inspect the JSON structure first"

Try It: Question the API

Ask api.vibecloud.dev/releases for the latest version, pull it out with jq -r, and leave just the number in version.txt.
Question the API

Loading playground...

9.4 Keys & Secrets

Every API you talk to wants a key, and keys are the one kind of text you must never type casually into a terminal. Here's the uncomfortable reason: your shell writes down everything you type. A key pasted into a command lives on in ~/.zsh_history — or ~/.bash_history if you're on bash — a plain text file that anyone who sits down at your machine can read, and search with the tricks in History Superpowers.

Keys live in files — never in commands
Caution
Never put a secret directly in a command. Not in curl -H, not in an export you type by hand, not in a script you commit. It lands in your history, in your terminal scrollback — the output your window is still holding, which is what a screen-share shows — and often in logs you don't control. Unlike a password, an API key is usually a straight line to a bill.

The standard answer is a .env file — "env" for environment, and nothing inside it but plain-text NAME=value lines, one per key. The file is locked down, and your commands reference the variable instead of the value. That's the environment variables and permissions from Permissions & Config doing real work together:

Keys in a file, not in a command
echo 'API_KEY=sk-vibe-9c2f10ab' > .env    # the key lives here
chmod 600 .env                          # only you can read it
source .env                             # run it in THIS shell, so the value sticks

curl -H "Authorization: Bearer $API_KEY" https://api.example.com/deploy
#                                 ↑ the variable, never the key itself

echo ".env" >> .gitignore               # and never commit it

That header line has more moving parts than it looks. Authorization is the header's name, the colon separates name from value, and Bearer is a fixed keyword saying what kind of credential follows — only the token at the end is yours. An API that wants X-API-Key: sk-vibe-9c2f10ab is the same line with a different name and no keyword at all.

The quotes change style mid-block on purpose. Single quotes are fully literal, which is what you want when writing the key into a file; double quotes still expand $API_KEY, which is what you want when handing its value to curl. That's the rule from Paths, and getting it backwards sends the server the eight characters $API_KEY instead of the key. source is what makes the variable available to that curl at all, for the reason in Your Shell Config.

chmod 600 is the Permissions & Config permission lesson with the stakes turned up: read and write for you, nothing at all for anyone else. And .gitignore keeps the key out of your repository — because a key pushed to GitHub is a key that belongs to the internet, usually within minutes.

One catch before you lean on that last line: a .gitignore entry stops git starting to track a file and does nothing at all for one it already tracks — which is exactly where you are if the key is already committed. Then the only fix is to revoke and reissue the key, which you do on the provider's website rather than in the terminal. It works because revoking kills the key on their servers; deleting the commit leaves it alive.

Tip
Agents leak keys by accident. An assistant that can read your files can read your .env, and one that writes code may inline a key it found "to make the example runnable." When you review agent-written code, grep for the shape of a key — grep -rn 'sk-' . — before you commit anything.
Vibe it

"Move the hard-coded API key in this script into a .env file and show me the safest way to load it"

"I accidentally committed an API key — what do I actually need to do, in order?"

Try It: Keep the Key Secret

deploy.sh has a key typed straight into it. Move it into .env, lock the file with chmod 600, and point the script at $API_KEY instead — with a .bak, naturally.
Keep the Key Secret

Loading playground...

9.5 A Door to Another Machine

One last idea, and it reframes everything you've learned: with ssh — secure shell, logging into another machine over the network — the same terminal can drive a different computer: a server in a data center, a Raspberry Pi in your closet, a cloud box that runs your app. Every command in this course works there, unchanged.

Same conversation, different machine
Stepping through the door
vibe@laptop:~$ ssh vibe@garden.example.com
vibe@garden:~$                  # the prompt changed — you are THERE now

vibe@garden:~$ ls               # this lists the server's files
vibe@garden:~$ exit             # back to your own machine
vibe@laptop:~$

This is where Anatomy of a Prompt from the introduction earns its keep: the prompt tells you which machine you're on. That is not a cosmetic detail. A rm -rf typed on the wrong side of that door is a very different afternoon, and the habit of glancing at the hostname before running anything destructive is one experienced people never drop.

You log in with a key pair rather than a password: two matching files in ~/.ssh. The public half is id_ed25519.pub — the .pub is what tells them apart — and it goes onto the server, where it acts as a lock anyone may see; the private half, id_ed25519, never leaves your machine. "Goes onto the server" is a concrete act: that public half gets appended to ~/.ssh/authorized_keys on the far side. Same rule as Keys & Secrets, in a different costume: the secret stays in a file, on your computer, with tight permissions.

Moving files through the door

ssh carries your keystrokes through the door; its sibling scp ("secure copy") carries files — same door, same keys. It reads exactly like the cp you know from Copy, Move, Delete, source first, destination second, except either side may live on another machine, and a colon marks which:

scpcp, across machines
scp status.json vibe@garden.example.com:~/reports/   # up: here -> there
scp vibe@garden.example.com:~/logs/server.log .      # down: there -> here
scp -r site/ vibe@garden.example.com:~/www/          # -r for folders, like cp

That colon is load-bearing: leave it off the destination and nothing crosses the network — scp status.json vibe@garden.example.com just makes a local file named like an address, quietly, in the folder you're standing in. The ls-your-target habit from Copy, Move, Delete catches it.

The heavy-duty version — and the one agents reach for in deploy scripts — is rsync: it compares the two sides first and sends only what changed, so the second run of a big copy takes seconds, and an interrupted one picks up where it stopped.

rsync — send only what changed
rsync -avz site/ vibe@garden.example.com:~/www/
# -a archive: keep permissions and timestamps    -v narrate    -z compress on the wire

rsync -avz --dry-run site/ vibe@garden.example.com:~/www/   # rehearse first

One sharp edge worth respecting: on the source, a trailing slash means "the contents of" while no slash means "the folder itself" — site/ fills ~/www/ directly, site creates ~/www/site inside it. The difference between the two is a nested folder you didn't want, which is exactly why --dry-run — the rehearsal flag from Read Before You Run's audit routine — earns a place in the command before the real run. Echo-the-glob, at server scale.

Tip
This is a real-machine lesson — the sandbox has no other computer to visit. When you do have a server to reach, ask your assistant to walk you through generating a key with ssh-keygen and installing it; it's a five-minute setup you do once per machine.
Vibe it

"Walk me through setting up an ssh key so I can log into my server without a password"

"What is the difference between my public and private ssh key, and which one goes where?"

You can now start a server, prove it's alive, read what it says, keep its keys safe, and step onto another machine entirely. One more part of tooling to go — how to get new tools, unpack what arrives, and find out what's eating your disk.

Challenge: Prove the Release

Loading challenge...