-
Notifications
You must be signed in to change notification settings - Fork 4
CI CD & Scripting
Patterns for using jamf-cli in automated environments.
Pass --no-input in CI/CD. Commands that need input then fail with an error instead of hanging.
jamf-cli pro computers list --no-input -o jsonImportant: under
--no-input, destructive operations (delete, erase, lock, wipe) fail with an error unless--yesis also passed.
Since v1.26.0: the "new jamf-cli release" advisory (see Configuration & Profiles#Release Update Notice (v1.26.0+)) stays silent in a pipeline. Any of
CI,CONTINUOUS_INTEGRATION,BUILD_NUMBER,RUN_ID,GITHUB_ACTIONS,GITLAB_CIorTF_BUILDvetoes it, and so does stdout or stderr not being a terminal. No flag is needed. To turn it off in an interactive shell inside a container that sets none of those markers, pass--no-update-checkor exportJAMF_CLI_NO_UPDATE_CHECK=1.
Destructive commands (delete, delete-multiple, erase, lock, wipe, remove, restart, shutdown) need explicit confirmation. Pass both --no-input and --yes to run them unattended:
# Will fail: --no-input blocks the confirmation prompt
jamf-cli pro scripts delete 42 --no-input
# Correct: --yes skips the confirmation
jamf-cli pro scripts delete 42 --no-input --yespro open, protect open, school open and security open resolve a console URL. In a pipeline they print it and launch nothing, so a job gets the URL rather than a failed browser launch. The URL is printed under any of --print, --no-input, --dry-run, --out-file, --field, --select, or a non-terminal stdout, which covers every CI invocation without a flag.
# The scripting form
jamf-cli pro open policies --field url
# https://your-instance.jamfcloud.com/policies.html
jamf-cli pro open --list -o json # every section name, no credentials neededIn the launch case stdout stays empty and the note goes to stderr, so nothing downstream reads a launch as data.
pro open is the only one of the four that can send a request: on a platform gateway profile it reads the instance URL from GET /v1/jamf-pro-server-url (scope jss-url:read), because a platform integration names a tenant and not a Jamf Pro host. On an instance profile it uses the configured URL and sends nothing. The other three send no request at all.
pro jamf-pro-notifications delete --all --yes -n sent a live tenant-wide DELETE and got a 204 in v1.29.0. Wire-checked before and after. A destructive generated command declares its own --dry-run/-n, which shadows the root persistent flag, so the template's own branch is the only thing honouring -n, and it sat after the --all block, which had already sent the request. Fixed in v1.30.0: the --all block previews before both the confirmation and the request.
Non-destructive bulk --all was never affected: pro app-installers-deployments installation-retry --all -n declares no local --dry-run, so dryRunClient covered it. If a pipeline uses -n as a safety net on a destructive --all, pin jamf-cli to v1.30.0 or later.
The platform gateway enables both Pro API and Platform API commands. The base URL is https://{region}.api.jamfcloud.com with no /api segment, and the CLI refuses a profile or JAMF_URL still naming the retired pre-GA host {region}.apigw.jamf.com by name before sending. See Platform API GA Migration.
You create an API integration at one of three scope levels in Jamf Account, and its credential works with that level alone, so pick the variable that matches the integration:
export JAMF_URL="https://eu.api.jamfcloud.com"
export JAMF_CLIENT_ID="your-client-id"
export JAMF_CLIENT_SECRET="your-client-secret"
# Platform environment scope: the level to prefer
export JAMF_ENVIRONMENT_ID="your-environment-id"
# ...or tenant scope (legacy)
# export JAMF_TENANT_ID="your-tenant-id"
# ...or organization scope: neither ID. The gateway host alone selects platform auth.
# Pro API commands work through the gateway
jamf-cli pro computers list --no-input -o json
# Platform API commands are also available
jamf-cli pro blueprints list --no-input -o json
jamf-cli pro ddm-reports device declarations <device-id> --filter 'active=in=(true,false)' --no-input -o json # --filter is requiredSetting JAMF_ENVIRONMENT_ID and JAMF_TENANT_ID together is refused at exit 2. Either one supplied in the environment replaces a profile's scope, the same as every other JAMF_* override. The equivalent flags are --environment-id and --tenant-id.
Refusals key on the resolved auth method. A CI job supplying
JAMF_URL+JAMF_CLIENT_ID+JAMF_CLIENT_SECRET(plus a scope ID, or the gateway host alone) gets the same pre-flight refusals a named profile would, in both directions, and before the token exchange, so on invalid credentials too. See Refused commands on a gateway profile below.
For on-premises instances or when platform gateway credentials are unavailable:
export JAMF_URL="https://jamf.company.com"
export JAMF_CLIENT_ID="your-client-id"
export JAMF_CLIENT_SECRET="your-client-secret"
jamf-cli pro computers list --no-input -o jsonexport JAMFPROTECT_URL="https://tenant.protect.jamfcloud.com"
export JAMFPROTECT_CLIENT_ID="your-client-id"
export JAMFPROTECT_CLIENT_SECRET="your-client-secret"
jamf-cli protect plans list --no-input -o jsonNote: The CLI also checks the generic
JAMF_URL,JAMF_CLIENT_IDandJAMF_CLIENT_SECRETas fallbacks for Protect commands.
export JAMFSCHOOL_URL="https://tenant.jamfschool.com"
export JAMFSCHOOL_NETWORK_ID="your-network-id"
export JAMFSCHOOL_API_KEY="your-api-key"
jamf-cli school devices list --no-input -o jsonNote:
JAMFSCHOOL_URLfalls back toJAMF_URLwhen unset. To also enable Platform API commands (blueprints, DDM reports), setJAMFSCHOOL_PLATFORM_URL,JAMF_CLIENT_ID,JAMF_CLIENT_SECRETandJAMF_TENANT_ID.
Read a token from a mounted secret file:
jamf-cli pro computers list --token-file /run/secrets/jamf-token --no-input -o jsonCreate a config file that references CI environment variables:
default-profile: ci
profiles:
ci:
url: https://jamf.company.com
auth-method: oauth2
client-id: env:JAMF_CLIENT_ID
client-secret: env:JAMF_CLIENT_SECRETsetup-jamf-cli action (recommended) installs in one line and handles platform detection, checksum verification, and PATH setup:
- uses: Jamf-Concepts/setup-jamf-cli@v1Available inputs:
| Input | Description | Default |
|---|---|---|
version |
Version to install (1.5.0, v1.5.0, or latest) |
latest |
token |
GitHub token used to resolve the latest version | ${{ github.token }} |
no-color |
Set NO_COLOR=1 for all subsequent steps |
false |
extra-args |
Default flags prepended via JAMF_CLI_ARGS (e.g. --quiet --no-input) |
'' |
Available outputs:
| Output | Description |
|---|---|
version |
Installed version without v prefix |
Supports ubuntu-latest and macos-latest. Windows runners are unsupported.
From source requires Go, and suits a specific unreleased commit:
- uses: actions/setup-go@v5
with:
go-version: 'stable'
- name: Install jamf-cli
run: go install github.com/Jamf-Concepts/jamf-cli/cmd/jamf-cli@latestname: Fleet Report
on:
schedule:
- cron: '0 8 * * 1' # Monday 8 AM
jobs:
report:
runs-on: ubuntu-latest
steps:
- uses: Jamf-Concepts/setup-jamf-cli@v1
with:
no-color: 'true'
extra-args: '--quiet --no-input'
- name: Export inventory
env:
JAMF_URL: ${{ secrets.JAMF_URL }}
JAMF_CLIENT_ID: ${{ secrets.JAMF_CLIENT_ID }}
JAMF_CLIENT_SECRET: ${{ secrets.JAMF_CLIENT_SECRET }}
run: |
jamf-cli pro computers list --all -o csv --out-file computers.csv
jamf-cli pro mobile-devices list --all -o csv --out-file mobile-devices.csv
- name: Upload artifacts
uses: actions/upload-artifact@v4
with:
name: fleet-report
path: |
computers.csv
mobile-devices.csvNote:
extra-args: '--quiet --no-input'prepends those flags to everyjamf-clicall in subsequent steps viaJAMF_CLI_ARGS, so there is no need to repeat them per command.
name: Platform Compliance Check
on:
schedule:
- cron: '0 9 * * 1' # Monday 9 AM
jobs:
compliance:
runs-on: ubuntu-latest
steps:
- uses: Jamf-Concepts/setup-jamf-cli@v1
with:
no-color: 'true'
extra-args: '--quiet --no-input'
- name: Check compliance and blueprint status
env:
JAMF_URL: ${{ secrets.JAMF_GATEWAY_URL }}
JAMF_CLIENT_ID: ${{ secrets.JAMF_CLIENT_ID }}
JAMF_CLIENT_SECRET: ${{ secrets.JAMF_CLIENT_SECRET }}
JAMF_TENANT_ID: ${{ secrets.JAMF_TENANT_ID }}
run: |
jamf-cli pro report blueprint-status -o json > blueprint-status.json
jamf-cli pro report ddm-status -o json > ddm-status.json
jamf-cli pro audit --checks platform -o json > audit.json
- name: Upload reports
uses: actions/upload-artifact@v4
with:
name: platform-reports
path: |
blueprint-status.json
ddm-status.json
audit.json - uses: Jamf-Concepts/setup-jamf-cli@v1
- name: Verify Jamf Pro health
env:
JAMF_URL: ${{ secrets.JAMF_URL }}
JAMF_CLIENT_ID: ${{ secrets.JAMF_CLIENT_ID }}
JAMF_CLIENT_SECRET: ${{ secrets.JAMF_CLIENT_SECRET }}
run: |
jamf-cli pro computers list --no-input -o json > /dev/null
echo "Jamf Pro connection verified"Sync Jamf Pro resources from a Git repository. apply is idempotent: it creates resources that do not exist and replaces ones that do, so it is safe to run on every merge.
name: Sync Jamf Pro Config
on:
push:
branches: [main]
paths: ['jamf-pro/**']
jobs:
sync:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: Jamf-Concepts/setup-jamf-cli@v1
with:
no-color: 'true'
extra-args: '--quiet --no-input'
- name: Apply buildings
env:
JAMF_URL: ${{ secrets.JAMF_URL }}
JAMF_CLIENT_ID: ${{ secrets.JAMF_CLIENT_ID }}
JAMF_CLIENT_SECRET: ${{ secrets.JAMF_CLIENT_SECRET }}
run: |
for f in jamf-pro/buildings/*.json; do
jamf-cli pro buildings apply --from-file "$f" --yes
done
- name: Apply categories
env:
JAMF_URL: ${{ secrets.JAMF_URL }}
JAMF_CLIENT_ID: ${{ secrets.JAMF_CLIENT_ID }}
JAMF_CLIENT_SECRET: ${{ secrets.JAMF_CLIENT_SECRET }}
run: |
for f in jamf-pro/categories/*.json; do
jamf-cli pro categories apply --from-file "$f" --yes
doneSync community analytics from a Git repository into your Protect tenant on every merge to main:
name: Sync Protect Analytics
on:
push:
branches: [main]
paths: ['analytics/**']
jobs:
sync:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: Jamf-Concepts/setup-jamf-cli@v1
with:
no-color: 'true'
- name: Import analytics
env:
JAMFPROTECT_URL: ${{ secrets.JAMFPROTECT_URL }}
JAMFPROTECT_CLIENT_ID: ${{ secrets.JAMFPROTECT_CLIENT_ID }}
JAMFPROTECT_CLIENT_SECRET: ${{ secrets.JAMFPROTECT_CLIENT_SECRET }}
run: |
jamf-cli protect analytics import --dir analytics/ --no-input
echo "Analytics sync complete"name: Protect Config Backup
on:
schedule:
- cron: '0 2 * * *' # Daily 2 AM
jobs:
backup:
runs-on: ubuntu-latest
steps:
- uses: Jamf-Concepts/setup-jamf-cli@v1
with:
no-color: 'true'
extra-args: '--no-input'
- name: Export Protect configuration
env:
JAMFPROTECT_URL: ${{ secrets.JAMFPROTECT_URL }}
JAMFPROTECT_CLIENT_ID: ${{ secrets.JAMFPROTECT_CLIENT_ID }}
JAMFPROTECT_CLIENT_SECRET: ${{ secrets.JAMFPROTECT_CLIENT_SECRET }}
run: |
# Whole-tenant capture, one portable document per object,
# in the same form `protect restore` replays
jamf-cli protect backup --output ./backup
- name: Upload artifacts
uses: actions/upload-artifact@v4
with:
name: protect-backup
path: backup/protect backup exits non-zero when any resource failed to export, so the job above fails on a partial capture; --allow-partial-failure tolerates one. It also prunes documents an earlier run left that no longer match the tenant, reporting each; --no-prune keeps them. Both take --resources and --exclude.
Two Protect resources capture third-party credentials verbatim: action-configs (an HTTP report client's request headers carry its bearer token or API key) and data-forwarding (its CloudFormation blob embeds a tenant-specific IAM ExternalId). The command writes those documents 0600 instead of 0644 and reports which ones they are.
Git records no non-exec permissions. A clone of a backup repository hands every file back
0644, so the mode does not survive the round trip. A pipeline committing a Protect backup to a repository should either exclude those two resources (--exclude action-configs,data-forwarding) or treat the repository itself as a secret store: private, with access controlled the way you would control the credentials it contains.
- uses: Jamf-Concepts/setup-jamf-cli@v1
with:
version: 'v1.5.0' - uses: Jamf-Concepts/setup-jamf-cli@v1
id: setup
- run: echo "Installed jamf-cli ${{ steps.setup.outputs.version }}"There is no published Docker image for jamf-cli. Download the binary in your Dockerfile.
FROM alpine:3 AS base
RUN apk add --no-cache curl jq ca-certificates
# Download the latest release binary
RUN ASSET="jamf-cli_$(curl -sL https://api.github.com/repos/Jamf-Concepts/jamf-cli/releases/latest \
| jq -r '.tag_name' | sed 's/^v//')_linux_amd64.tar.gz" \
&& curl -sL "https://github.com/Jamf-Concepts/jamf-cli/releases/latest/download/${ASSET}" | tar xz -C /usr/local/bin/ jamf-cli
# --- Minimal runtime image ---
FROM alpine:3
RUN apk add --no-cache ca-certificates
COPY --from=base /usr/local/bin/jamf-cli /usr/local/bin/jamf-cli
ENTRYPOINT ["jamf-cli"]Build it:
docker build -t jamf-cli .# Jamf Pro
docker run --rm \
-e JAMF_URL=https://jamf.company.com \
-e JAMF_CLIENT_ID=abc123 \
-e JAMF_CLIENT_SECRET=secret \
jamf-cli pro computers list --no-input -o json
# Jamf Protect
docker run --rm \
-e JAMFPROTECT_URL=https://tenant.protect.jamfcloud.com \
-e JAMFPROTECT_CLIENT_ID=abc123 \
-e JAMFPROTECT_CLIENT_SECRET=secret \
jamf-cli protect plans list --no-input -o jsonFor Kubernetes or Docker Swarm where secrets are mounted as files, use the file: prefix in your config:
# config.yaml
default-profile: container
profiles:
container:
url: https://jamf.company.com
auth-method: oauth2
client-id: file:/run/secrets/jamf-client-id
client-secret: file:/run/secrets/jamf-client-secretdocker run --rm \
-v /path/to/config.yaml:/root/.config/jamf-cli/config.yaml:ro \
-v /path/to/client-id:/run/secrets/jamf-client-id:ro \
-v /path/to/client-secret:/run/secrets/jamf-client-secret:ro \
jamf-cli pro computers list --no-input -o jsonIn Kubernetes, mount the config as a ConfigMap and the secrets as a Secret volume at /run/secrets/.
Set JAMF_CLI_ARGS to prepend default flags to every invocation without modifying individual scripts:
# Always use quiet, non-interactive mode
export JAMF_CLI_ARGS='--quiet --no-input'
# Always use a specific profile
export JAMF_CLI_ARGS='--profile prod'
# Profile names with spaces work: shell quoting is supported
export JAMF_CLI_ARGS='--profile "My CI Profile"'
# Combine flags
export JAMF_CLI_ARGS='--no-input --no-color -o json'The CLI parses JAMF_CLI_ARGS with full shell quoting, so a value containing spaces works, and prepends the flags before any command-line argument, so a per-invocation flag takes precedence.
-oinJAMF_CLI_ARGSbreaks the two backup commands.pro backupandprotect backuptake--outputas a destination directory, which shadows the global output-format flag and takes the-oshorthand with it, so those invocations exit 2 withunknown shorthand flag: 'o' in -o.JAMF_CLI_ARGSis prepended to every call, so one pipeline-wide-o jsonbreaks the backup step of an otherwise working pipeline. Keep-oout ofJAMF_CLI_ARGSwhere a job also runs a backup and set it per command; those commands write their files in the format--format yaml|jsonnames. Details below: The-oshorthand is gone on the backup commands.
For machine parsing, combine these flags:
jamf-cli pro computers list \
--no-input \ # never prompt
--no-color \ # no ANSI escape codes
-o json # structured output-
JSON (
-o json): the most reliable for machine use. Errors are JSON too (see Error Handling & Exit Codes). -
CSV (
-o csv): good for spreadsheet imports and simple parsing. -
Plain (
-o plain): tab-separated, no headers. Good forcut,awk,sort. -
--field: extract a single field withoutjq. Suits piping IDs into loops:
# Simpler than: jamf-cli pro scripts list -o json | jq -r '.[].id'
jamf-cli pro scripts list --field id --no-input# Quiet mode: suppress spinner and non-error messages
jamf-cli pro computers list -q -o jsonFive changes tightened what the CLI accepts. Four of them answer exit 2, where the same mistakes used to exit 1, so a wrapper treating exit 1 as a bad invocation has to key on 2.
| Invocation | Answer |
|---|---|
| A missing required flag; a flag group with no member set; a flag group whose mutually exclusive members are set together; the wrong number of positionals | Exit 2 (usage). 48 call sites declare a required flag, 118 declare a flag group, 671 validate an argument count. |
A stray positional on a leaf that documents none, such as pro categories list junkarg or pro backup /tmp/out
|
Exit 2. 736 leaves used to accept one and discard it, so a job passing an extra token now fails where it appeared to succeed. |
--file on a Platform or Security Cloud write |
unknown flag: --file, exit 2. The request-body flag is --from-file in every product, with no compatibility alias. The error names the replacement, and under -o json it is the envelope's hint. |
A pro resource name retired in v1.29.0 |
Still resolves, until 2027-03-09, with a stderr warning that neither --quiet nor --no-hints silences. |
| A moved operation name; a former leaf that is now a command group, asked for data; a verb whose meaning moved | Exit 2. Three withdrawn names exit 1. |
# Before
jamf-cli security ztna-gateways create --from-file gateway.yaml
# After
jamf-cli security ztna-gateways create --from-file gateway.yaml--file is unchanged on the 11 commands where it names a multipart upload payload (pro packages upload, protect analytics import, and nine others). A --from-file body can arrive on a pipe instead; an upload cannot, the transport needing a filename and a length.
The rename warning:
warning: `computers-inventory` is a deprecated name for `computer-inventory` and stops working after
2027-03-09. Use `pro computer-inventory`.
Pro Command Renames has the old→new table; the refusal wording and exit codes are in Error Handling & Exit Codes#A retired pro command name.
The group-parent case is the one a pipeline hits most often. pro csas token used to return data; the path still resolves, and a bare invocation prints help at exit 0. An invocation carrying -o, --field, --select or --out-file is refused at exit 2 with the leaf named:
$ jamf-cli pro csa token --field id
{
"error": "usage",
"exitCode": 2,
"exitCodeName": "usage",
"hint": "run `jamf-cli pro csa token get`",
"message": "`pro csa token` is a command group and returns no data; it returned data before the sub-resource split"
}
One change breaks existing scripts: the Classic scope subcommands take an id, not a bare name.
| Invocation | Answer |
|---|---|
pro classic-policies scope get "Deploy Chrome", a non-numeric positional on any Classic scope get/add/remove
|
Exit 2, naming --name. Through v1.29.0 the positional was the name. |
A --name that matches more than one record |
Refused. Classic names are not unique, and it used to resolve to the first in document order. |
A positional and --name together |
Refused, matching every other Classic command. |
# Before (v1.29.0)
jamf-cli pro classic-policies scope get "Deploy Chrome"
# After (v1.30.0): either form
jamf-cli pro classic-policies scope get 1
jamf-cli pro classic-policies scope get --name "Deploy Chrome"Nine resources carry scope: classic-policies, classic-macos-config-profiles, classic-mobile-config-profiles, classic-mac-apps, classic-mobile-apps, classic-ebooks, classic-restricted-software, classic-vpp-assignments and classic-vpp-invitations. Each registers only the scope categories its own resource accepts, so a cross-family flag now arrives as cobra's unknown flag at exit 2 with the accepted categories named in the hint.
-n / --dry-run works on these commands in v1.30.0. It used to always fail: the preview suppressed the PUT and the post-write check then reported the server had not persisted it.
On a platform gateway credential, a Jamf Pro or Classic command whose endpoint sits outside the gateway's published API is refused before the request is sent, with exit code 8 (unsupported). A Platform-only command on an instance profile is refused the same way.
Exit 2 covers every flag error, unknown subcommand, missing URL, missing credential, the retired-host refusal and the scope conflict, so key on 8 to tell a policy refusal from a mistyped command:
jamf-cli pro mobile-devices lock --serial F4GH5678 --yes --no-input -o json
case $? in
0) echo "sent" ;;
8) echo "not served on this credential: falling back to the instance profile"
jamf-cli -p jamf-instance pro mobile-devices lock --serial F4GH5678 --yes --no-input -o json ;;
*) exit 1 ;;
esacAudit the whole surface up front. commands needs no auth, so this runs in any job:
jamf-cli commands -o json | jq -r '.[] | select(.gateway=="unserved") | .command + "\t" + .gatewayBasis'POST /v2/mdm/commands is unpublished, so all 24 MDM device actions under pro mobile-devices (16) and pro computer-inventory (8) are refused on a gateway profile, 24 of the 59 refused entries in this binary. A job that issues device commands needs a second oauth2 profile pointed at the instance.
JAMF_CLI_ALLOW_UNPUBLISHED=1 downgrades an unpublished refusal to a stderr warning and sends the request. Treat it as a stopgap for one job. It is value-parsed with Go's strconv.ParseBool, so a runner that exports it can turn it off with JAMF_CLI_ALLOW_UNPUBLISHED=0 instead of unsetting it. Neither --quiet nor --no-hints silences the warning. Full detail in Error Handling & Exit Codes#Refused by Policy (exit 8) and Platform API GA Migration.
commands -o json carries preview: true on every command whose endpoint upstream declares Preview, sourced from the spec's per-operation x-preview. Thirteen commands carry it in v1.30.0: platform ai-policies × 10 (including the apply the CLI synthesizes) and platform ai-tools × 3.
jamf-cli commands -o json | jq -r '.[] | select(.preview) | .command'The field is positive-only, like every other catalog field: most specs declare nothing, so its absence means "nothing recorded" and not "this one is GA".
Twelve of the thirteen also have a description opening with Preview - (Preview - List active AI governance policies for the tenant), the synthesized apply being the exception. That prefix is upstream summary prose nobody here controls, so a job that wants to refuse Preview endpoints reads preview.
pro backup and protect backup take --output as a destination directory. Cobra drops an inherited persistent flag whose name a local one already takes, and the shorthand goes with it, so -o does not exist on those two commands:
$ jamf-cli pro backup -o json --output ./backup
unknown shorthand flag: 'o' in -o # exit 2This bites hardest through JAMF_CLI_ARGS, which is prepended to every invocation, so export JAMF_CLI_ARGS='-o json' breaks the backup step of an otherwise working pipeline. Use --format yaml|json, these commands' own switch for the files they write, and keep -o out of JAMF_CLI_ARGS for those steps or set it per command.
Since v1.19.0, batch commands return exit code 7 (partial_failure) when some items succeed and some fail, distinct from a total failure: pro bulk *, generated bulk-delete, pro backup, protect backup and protect restore.
The three backup/restore commands exit non-zero by default: a backup that exits 0 with a resource missing looks the same as a good one to the job that scheduled it. --allow-partial-failure (a global flag) downgrades it to a warning and exit 0. In CI you can treat 7 as a hard failure or tolerate it:
# Fail the job on any partial failure (default: exit 7 is non-zero)
jamf-cli pro bulk enable-policies --category "Onboarding" --yes -o json
# Best-effort: keep going and exit 0 even if a few items failed
jamf-cli pro bulk enable-policies --category "Onboarding" --yes \
--allow-partial-failure -o json
# Or branch on the code explicitly
jamf-cli pro backup --output ./backup --no-input -o json
case $? in
0) echo "All objects exported" ;;
7) echo "Partial export: some objects failed (see warnings)"; exit 1 ;;
*) echo "Backup failed entirely"; exit 1 ;;
esacThe JSON envelope on a partial failure carries succeeded / failed counts alongside the standard error fields; see Error Handling & Exit Codes.
#!/bin/bash
set -euo pipefail
# Ensure required env vars
: "${JAMF_URL:?JAMF_URL must be set}"
: "${JAMF_CLIENT_ID:?JAMF_CLIENT_ID must be set}"
: "${JAMF_CLIENT_SECRET:?JAMF_CLIENT_SECRET must be set}"
# Fetch all computers
computers=$(jamf-cli pro computers list --all --no-input --no-color -o json)
# Process results
count=$(echo "$computers" | jq length)
echo "Found $count computers"
# Check for stale devices (no check-in for 30+ days)
stale=$(echo "$computers" | jq '[.[] | select(.lastContactDate < "2025-01-01")] | length')
echo "Stale devices: $stale"
# Exit based on threshold
if [ "$stale" -gt 100 ]; then
echo "WARNING: Too many stale devices"
exit 1
fi#!/bin/bash
set -euo pipefail
# Ensure required env vars
: "${JAMFPROTECT_URL:?JAMFPROTECT_URL must be set}"
: "${JAMFPROTECT_CLIENT_ID:?JAMFPROTECT_CLIENT_ID must be set}"
: "${JAMFPROTECT_CLIENT_SECRET:?JAMFPROTECT_CLIENT_SECRET must be set}"
CLI="jamf-cli protect --no-input --no-color"
# Fetch overview data
plans=$($CLI plans list -o json)
analytics=$($CLI analytics list -o json)
computers=$($CLI computers list -o json)
echo "Plans: $(echo "$plans" | jq length)"
echo "Analytics: $(echo "$analytics" | jq length)"
echo "Computers: $(echo "$computers" | jq length)"
# Export all plans for backup
echo "$plans" | jq -r '.[].name' | while read name; do
$CLI plans export "$name" > "backup/${name}.json"
echo "Exported plan: $name"
doneRepository · Issues · Releases
jamf-cli Wiki
- Home
- Community
- Getting Started
- CLI Reference
- Product Commands
- Workflows
- Configuration
- Reference
Products
- Jamf Pro:
jamf-cli pro - Jamf Platform API:
jamf-cli pro(blueprints, benchmarks, DDM reports) - Jamf Platform:
jamf-cli platform(AI Governance, Jamf Account, audit) - Jamf Protect:
jamf-cli protect - Jamf School:
jamf-cli school - Jamf Security Cloud:
jamf-cli security