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.
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:
http://localhost:3000/api/health
└─┬─┘ └───┬───┘ └─┬┘ └────┬────┘
│ │ │ └── which room inside — the path
│ │ └── which door — the port
│ └── which machine — here, this one
└── the language — httpThe 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.
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."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:
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 headersFour 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.
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."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?
curl, then save the
reply to status.json with -s -o so you have the receipt.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.
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.0A 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.
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."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
api.vibecloud.dev/releases for the latest version, pull it out with jq -r, and leave just the number in version.txt.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.
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:
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 itThat 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.
.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."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.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.
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:
scp — cp, 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 cpThat 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 firstOne 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.
ssh-keygen and installing it; it's a five-minute setup you do once per machine."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.
Loading challenge...