First, what is this thing?
axis-cli is a program you run by typing commands. It talks to your camera over
the network using VAPIX — the same HTTP API the camera's own web interface uses.
Anything you can click in the camera's web page, you can script here instead.
The one idea that makes it pleasant: you save each camera once as a named profile, then refer to it by that name forever. No retyping IP addresses, no retyping passwords.
It is not a daemon
Nothing runs in the background. You type a command, it talks to the camera, prints the answer, and exits.
It runs on your computer
Not on the camera. Install it on your Mac or PC; it reaches cameras over your LAN or via CamStreamer Cloud.
Credentials are saved once
Profiles live in ~/.axis-cli/config.json, permissions 0600. They survive reboots.
Open a terminal
This is the window where you type commands. You'll keep it open for the rest of this tutorial.
- macOS — press
⌘ Space, type Terminal, press Return. - Windows — press the Windows key, type Terminal (or PowerShell), press Enter.
You'll see a line ending in $, %, or >. That's the prompt —
it means the terminal is waiting for you. In the examples below, that leading
symbol is not something you type.
Don't paste a command that has a # comment on the end of the line. In macOS's
default zsh shell, interactive comments are off, so the comment gets passed to the program as
arguments and you get a confusing error. Copy the command only.
Install axis-cli
You need Node.js 18 or newer. The installer scripts check for it and tell you what to do if it's missing.
- Download
axis-cli-v1.1.2-installer.zipfrom the latest release. - Unzip it. You get a folder called
axis-cli. - Double-click
install.commandinside that folder.
The script isn't signed by an Apple developer account, so Gatekeeper refuses a plain
double-click. Right-click install.command → Open, then confirm.
You only do this once. If Finder won't run it at all, open Terminal and run:
cd ~/Downloads/axis-cli
chmod +x install.command
./install.command
The installer checks Node, installs dependencies, builds, installs the axis command globally, and verifies it. Expect output like:
==> Checking Node.js
ok Node v22.14.0 (/opt/homebrew/bin/node)
ok npm 10.9.2
==> Installing dependencies
ok Dependencies ready
==> Building TypeScript
ok Built dist/index.js
==> Installing 'axis' globally
ok Installed into /opt/homebrew
==> Verifying
ok axis 1.1.2 (/opt/homebrew/bin/axis)
- Download
axis-cli-v1.1.2-installer.zipfrom the latest release. - Right-click the zip → Extract All. Extracting matters — running the script from inside the zip preview will fail.
- Double-click
install.bat.
If you don't have Node.js yet, install it first and then close and reopen the window so the new PATH takes effect:
winget install OpenJS.NodeJS.LTS
The global install folder needs elevated rights. Right-click install.bat → Run as administrator.
git clone https://github.com/kotyzap/axis-cli.git
cd axis-cli
npm ci
npm run build
npm install -g .
axis --version
npm install -g . — not npm link
npm link only creates a symlink pointing back at the project folder. Move that
folder, delete it, or keep it on an external drive that isn't mounted, and axis
breaks. npm install -g . copies the built files into Node's global folder, so
it's self-contained.
Check it worked
axis --version
1.1.2
That's permanent. You do not reinstall after a reboot — the axis
command lives in Node's global folder on your system drive and stays on your PATH.
If your Node came from nvm, a global install belongs to that one Node version.
Switch versions and axis seems to vanish — just re-run the installer. Installing
Node via Homebrew (brew install node) sidesteps this.
Find your Q1656 on the network
You need its IP address. Pick whichever you already have to hand:
- AXIS IP Utility or AXIS Device Manager — lists every Axis device on the subnet with model and IP. Easiest if you have it installed.
- Your router's DHCP client list — look for a hostname like
axis-b8a44f…or a MAC starting00:40:8C(Axis Communications). - The camera's own web page — if you already know the address, the IP is right there in the browser bar.
- From the terminal — after pinging the subnet, Axis devices show up by their
MAC prefix:
# macOS / Linux arp -a | grep -i "0:40:8c" # Windows arp -a | findstr "00-40-8c"
For the rest of this tutorial the Q1656 is at 192.168.1.50.
Substitute your own address everywhere you see it.
Open http://192.168.1.50 in a browser and log in. If that doesn't work,
axis-cli won't either — it's the same network path and the same credentials. Fix
it here first.
Add the camera
This is the step you were asking about — and the answer to "where does it ask for the username and password?" is: right here, once. Run:
axis camera add q1656 --ip 192.168.1.50 --user root
Notice there is no password on that line. Because you left --pass off, the CLI stops and asks you:
Password for root@192.168.1.50:
Type the camera's password and press Return. Nothing appears as you type — no dots, no asterisks, no movement at all. That's deliberate, not a frozen terminal. Then:
✓ Saved camera profile "q1656" to /Users/you/.axis-cli/config.json
→ Verify it with: axis camera test q1656
The password is stored in ~/.axis-cli/config.json (Windows:
%USERPROFILE%\.axis-cli\config.json), file mode 0600 — readable only
by your user account. Every later command just takes the profile name q1656.
What each part means
q1656- The profile name — your label, anything you like.
lobby,gate-north,q1656. This is what you'll type in every other command. --ip 192.168.1.50- The camera's address. A hostname works too. Note it's
--ip, not--host. --user root- The camera account name.
rootis the default, so you can omit this flag entirely — it's shown here to make the point that the username is a flag, not a prompt. --pass(omitted on purpose)- Leave it off to get the hidden prompt. See below for when you'd use it.
Three ways to supply the password
The CLI checks these in order and uses the first one it finds.
| Method | How | When to use it |
|---|---|---|
| 1. Hidden prompt | Omit --pass |
Default choice. Nothing lands in shell history or in ps output. |
| 2. Environment variable | AXIS_PASS |
Scripts, CI, cron — anywhere there's no terminal to prompt at. |
| 3. Flag | --pass hunter2 |
Last resort. The password is now in your shell history and visible to other processes. |
# macOS / Linux
export AXIS_PASS='your-password'
axis camera add q1656 --ip 192.168.1.50
# Windows PowerShell
$env:AXIS_PASS = 'your-password'
axis camera add q1656 --ip 192.168.1.50
Since AXIS OS 11 there is no factory default password — you set one on first boot. If you've
never logged into this camera, do that in the web interface first; there's no password for
axis-cli to use until you have.
Create a dedicated account in the camera's web UI (System → Users) and use that
instead. Administrator role is needed for param set,
reboot, and starting/stopping ACAPs; Operator is enough for PTZ
and read-only queries.
axis camera add q1656 --ip 192.168.1.50 --user cli-svc
Fixing or changing a profile
Change the stored password (bare --pass prompts again, hidden)
axis camera update q1656 --pass
Camera moved to a new address
axis camera update q1656 --ip 192.168.1.77
Wrong username
axis camera update q1656 --user cli-svc --pass
Start over
axis camera remove q1656
Verify it works
Always do this before anything else. It proves the network path and the credentials in one shot.
axis camera test q1656
✓ "q1656" reachable — AXIS Q1656 Box Camera (firmware 11.11.61)
If that came back green, you're done setting up. Confirm what you've saved:
axis camera list
┌───────┬────────────────────┬──────┬─────┐
│ Name │ Access │ User │ TLS │
├───────┼────────────────────┼──────┼─────┤
│ q1656 │ 192.168.1.50:80 │ root │ no │
└───────┴────────────────────┴──────┴─────┘
Passwords are never printed, not even with --json.
Run your first real commands
Every command follows the same shape: axis <area> <action> <profile>.
See what the camera is
axis info q1656
axis info q1656 --all-acaps also lists third-party ACAPs
Brand, model, firmware, serial, and which CamStreamer-family ACAPs are installed and running.
Read and write settings
axis param get q1656 root.Brand.ProdFullName root.Network.eth0.IPAddress
axis param set q1656 root.Time.DST=yes
These are the same parameters you'd find in the camera's plain-config page.
ACAP applications
axis apps list q1656
axis apps list q1656 --running
axis apps restart q1656 CamScripter
Watch live events
First, see what this camera actually publishes:
axis events topics q1656
┌─────────────────────────────────────────────────────────┐
│ Topic filter │
├─────────────────────────────────────────────────────────┤
│ tns1:Device/tnsaxis:IO/VirtualPort │
├─────────────────────────────────────────────────────────┤
│ tns1:VideoSource/tnsaxis:DayNightVision │
├─────────────────────────────────────────────────────────┤
│ tnsaxis:CameraApplicationPlatform/VMD/Camera1ProfileANY │
└─────────────────────────────────────────────────────────┘
Then stream them. With no --topic, you get everything:
axis events watch q1656
2026-07-29T09:14:02.199Z tns1:Device/tnsaxis:IO/VirtualPort port=1 state=1
2026-07-29T09:14:05.400Z tns1:Device/tnsaxis:IO/VirtualPort port=1 state=0
Or narrow it down — --topic is repeatable:
axis events watch q1656 --topic 'tns1:Device/tnsaxis:IO/VirtualPort'
axis events watch q1656 --topic 'tns1:Device//.' --topic 'tns1:VideoSource//.'
axis events watch q1656 --raw full JSON payload per event
Press Ctrl+C to stop — that's how you exit any long-running command.
They're the exact strings the camera declares, which is why
axis events topics exists — copy a value from there rather than guessing.
A trailing //. matches everything below a branch, so
tns1:Device//. covers every device event.
Axis recommends subscribing only to what you need: each subscription starts internal services on the camera, and subscribing to everything on a busy device can affect performance.
CamOverlay, CamStreamer, CamSwitcher
CamOverlay — toggle a graphic, push text into a field
axis overlay list q1656
axis overlay text q1656 2 temperature=22.5C
CamStreamer — start/stop a configured stream
axis stream list q1656
axis stream start q1656 42448
axis stream stop q1656 42448
CamSwitcher — change the live view
axis switcher switch q1656 MainView
stream start just flips a flag and returns immediately.
stream stop waits for the camera to gracefully close its connection to
the RTMP/HLS/SRT destination first, which can take longer — the CLI allows up to
20 seconds by default (10 for start). If you still see
"The operation was aborted due to timeout", add
--timeout 30000 rather than assuming something is broken.
Anything not wrapped yet
axis vapix get q1656 /axis-cgi/param.cgi action=list group=Image
axis --help lists every area. axis camera --help or
axis overlay text --help drills into one. Misspell a command and it suggests the
nearest match.
Add the rest of your fleet
Repeat step 4 for each camera, giving each a memorable name:
axis camera add lobby --ip 192.168.1.51
axis camera add gate-north --ip 192.168.1.52
axis camera add warehouse --ip 192.168.1.53
Now one command checks all of them, probed in parallel so a dead camera doesn't stall the report:
axis fleet health
Reachability, firmware, SD card status, and how many CamStreamer-family ACAPs are running. It exits with code 1 if any camera is unreachable, which is what makes it usable as a monitoring check rather than just a pretty table.
Cameras behind CamStreamer Cloud
No local IP? Use a device-connect.net URL and a DEVICE_ACCESS_TOKEN instead:
axis camera add remote-cam --cloud-url https://xxxx.device-connect.net
You'll be prompted for the token, hidden, exactly like a password (or set
AXIS_CLOUD_TOKEN). Note that axis events watch needs a local
--ip profile, since it opens a direct WebSocket to the camera.
Machine-readable output
Add --json anywhere on the line to get JSON instead of tables:
axis fleet health --json | jq '.[] | select(.reachable == false)'
Switch to HTTPS (optional)
Plain HTTP sends credentials over your LAN in the clear. If the camera has HTTPS enabled, add
--tls. Port then defaults to 443 instead of 80.
axis camera add q1656 --ip 192.168.1.50 --tls --tls-insecure
--tls-insecure accepts a self-signed certificate. Most Axis cameras ship with one,
so you usually need it — the connection is still encrypted, it just isn't verified against a CA.
Drop the flag once you've installed a proper certificate.
Switch an existing profile — the port moves to 443 with it
axis camera update q1656 --tls --tls-insecure
axis camera test q1656
Troubleshooting
The failures people actually hit, and what each one really means.
command not found: axis
The install worked but the folder holding axis isn't on your PATH. Find out where it went:
npm prefix -g
Then add that folder's bin to your shell profile, once:
echo 'export PATH="$(npm prefix -g)/bin:$PATH"' >> ~/.zshrc
source ~/.zshrc
On Windows, open a new terminal first — PATH changes don't reach windows that were already open.
axis worked yesterday and is gone today
Two usual causes:
- You used
npm linkand the project folder moved — or lives on an external drive that isn't mounted. Reinstall withnpm install -g ., which copies rather than symlinks. - nvm switched Node versions. Global installs belong to one Node version. Re-run the installer, or move to Homebrew Node.
zsh: permission denied: axis
The compiled entry point lost its executable bit — tsc doesn't set one, so anything
that regenerates dist/ leaves it mode 644. Re-run npm run build (the
postbuild step fixes the mode), or set it directly:
chmod +x dist/index.js
sh: tsc: command not found during the build
You have NODE_ENV=production set, so npm install skipped
devDependencies and TypeScript was never installed. Force them in:
npm install --include=dev
camera test fails with 401 Unauthorized
Reached the camera, credentials rejected. In order of likelihood:
- Wrong password. Retype it:
axis camera update q1656 --pass - Wrong username —
--userdefaults toroot, which may not be the account you meant.axis camera listshows what's stored. - The account exists but lacks the privilege for that operation. Reads work, writes need Administrator.
- A password with
$,!or"passed via--passgot mangled by your shell. Use the hidden prompt instead — it never touches shell quoting.
camera test times out or can't connect
Network, not credentials. Work outward:
- Open
http://192.168.1.50in a browser. If that fails too, it's not axis-cli. - Different VLAN or subnet? You need a route to the camera.
- Camera set to HTTPS only? Then plain HTTP is refused — add
--tls --tls-insecure. - Non-standard port:
axis camera update q1656 --port 8080 - Did the IP change? DHCP leases move. Check with AXIS IP Utility.
TLS error: self-signed certificate
Expected on a stock Axis camera. Add --tls-insecure to accept it. Traffic stays
encrypted; only CA verification is skipped.
axis camera update q1656 --tls-insecure
events watch: "Could not use supplied event filter"
The camera rejected a topic filter. On axis-cli 1.1.0 this happened on every
invocation — a bug: the CLI asked the camera to subscribe to a topic literally
called event. Upgrade to 1.1.1 or later.
On 1.1.1+, it means the filter you passed isn't one the camera accepts. Find the real ones:
axis events topics q1656
axis events watch q1656 --topic 'tns1:Device/tnsaxis:IO/VirtualPort'
Watch your shell quoting too — a filter containing // or : should
be wrapped in single quotes.
events watch can't connect over HTTPS
On 1.1.0, events watch silently ignored --tls-insecure, so a
--tls profile using the camera's stock self-signed certificate failed to open the
WebSocket. Fixed in 1.1.1. Also note that event watching needs a local --ip
profile — a CamStreamer Cloud profile can't open a direct WebSocket to the camera.
stream stop: "The operation was aborted due to timeout"
Not a broken stream — the camera hadn't finished tearing down its connection to the
streaming destination within the wait window. stream stop waits 20 seconds
by default; stream start only 10, since starting returns immediately while
stopping an actively-publishing stream has real teardown work to do.
If 20 seconds isn't enough for this camera or destination, raise it:
axis stream stop q1656 42448 --timeout 30000
Check axis stream show q1656 42448 afterwards — it usually reports
active: false even after a timeout, meaning the stop went through on the
camera side and only the CLI gave up waiting for the confirmation.
The password prompt never appears
The prompt needs a real terminal. Inside a script, a pipe, or a CI job there's no TTY, so the CLI
refuses to prompt and errors instead. Set AXIS_PASS in those contexts.
npm error EINVALIDTAGNAME ... Invalid tag name "#"
You pasted a command with a trailing # comment into interactive zsh. Unlike bash,
zsh doesn't treat # as a comment on the command line, so it becomes an argument.
Backticks inside such a comment are worse — zsh executes them. Paste the command without the
comment, or enable comments for the session with setopt interactive_comments.
Where is my config, and how do I start clean?
~/.axis-cli/config.json on macOS/Linux,
%USERPROFILE%\.axis-cli\config.json on Windows. Mode 0600.
Delete the file to wipe every profile. Uninstalling the CLI leaves it alone:
npm uninstall -g axis-cli
Credentials are stored in plaintext, so treat the file as a secret and never commit it anywhere.
Cheat sheet
Every command takes the profile name, never a raw IP.
| Goal | Command |
|---|---|
| Save a camera | axis camera add q1656 --ip 192.168.1.50 |
| Prove it works | axis camera test q1656 |
| List profiles | axis camera list |
| Change password | axis camera update q1656 --pass |
| Delete a profile | axis camera remove q1656 |
| Model & firmware | axis info q1656 |
| Read a parameter | axis param get q1656 root.Brand.ProdFullName |
| Write a parameter | axis param set q1656 root.Time.DST=yes |
| Go to a PTZ preset | axis ptz goto q1656 "Home" -c 1 |
| List ACAPs | axis apps list q1656 |
| Restart an ACAP | axis apps restart q1656 CamScripter |
| Toggle an overlay | axis overlay enable q1656 2 |
| Push overlay text | axis overlay text q1656 2 temp=22.5C |
| Start a stream | axis stream start q1656 42448 |
| Stop a stream | axis stream stop q1656 42448 --timeout 30000 |
| Switch the view | axis switcher switch q1656 MainView |
| Check every camera | axis fleet health |
| List event topics | axis events topics q1656 |
| Watch live events | axis events watch q1656 |
| Watch one topic | axis events watch q1656 --topic 'tns1:Device//.' |
| Reboot | axis reboot q1656 --yes |
| Raw VAPIX call | axis vapix get q1656 /axis-cgi/param.cgi action=list |
| JSON output | append --json to anything |
| Help | axis --help, axis <area> --help |
1.1.2 fixes axis stream stop timing out on
actively-publishing streams and adds --timeout to stream start/
stream stop. 1.1.1 repairs axis events watch,
which failed on every invocation in 1.1.0, and adds axis events topics.
1.1.0 changed two behaviours from 1.0.0: --tls now defaults to
port 443 (was 80), and axis reboot without --yes
refuses and exits 1. See the
changelog.