The official command-line client for HodeiShield, by Hodeitek. HodeiShield covers third-party risk, supply-chain security and compliance (NIS2, DORA, ISO 27001, ENS).
Security policy, how to report a vulnerability and where advisories appear: SECURITY.md.
# Download and unpack the archive for your platform from
# https://github.com/Hodeitek/hodeishield-cli/releases/latest, then:
hodeishield login # sign in through the browser and pick a tenant
hodeishield vendors list # your third-party inventoryOn a server, in a container or in CI there is usually no keychain and login stops with an error: use a
tenant API key instead.
Verify the download before you run it: see Install and Verify a download.
Read your third-party inventory, supply-chain alerts, risk register, compliance posture, evidence and endpoint agents from a terminal or a script.
- Read-only. Every command is a
GETagainst the public/v1API. Nothing here can change data in your organisation. - Safe with credentials. Sign-in tokens live in the system keychain unless you opt in to a plain-text file for machines that have none; nothing is ever printed or logged; credentials only travel over HTTPS and are never replayed to a redirect.
- Built for scripts.
--jsonprints the API's own JSON, exit codes say what went wrong, andHODEISHIELD_API_KEYworks without any interactive step. - Verifiable. Every release is signed with Sigstore (keyless, from GitHub Actions) and carries SLSA build provenance.
The CLI is at version 0.x: commands and output may still change before 1.0. See Stability for what may change and how it is announced.
Download the archive for your platform from the
latest release, verify it (next
section), and put hodeishield (hodeishield.exe on Windows) somewhere on your PATH. If macOS or
Windows blocks the downloaded binary, see
If macOS or Windows blocks the binary.
| Platform | Archive |
|---|---|
| Linux x86_64 | hodeishield-<version>-x86_64-unknown-linux-musl.tar.gz |
| Linux arm64 | hodeishield-<version>-aarch64-unknown-linux-musl.tar.gz |
| macOS (Apple silicon and Intel) | hodeishield-<version>-universal-apple-darwin.tar.gz |
| Windows x86_64 | hodeishield-<version>-x86_64-pc-windows-msvc.zip |
| Windows arm64 | hodeishield-<version>-aarch64-pc-windows-msvc.zip (from 0.4.0) |
<version> has no leading v: release v0.5.1 ships hodeishield-0.5.1-…. The Linux binaries are
statically linked and run on any distribution. Homebrew and Scoop packages (next section) and a
container image are also available.
On Linux you need curl (or another way to download), tar, gzip and a CA certificate bundle
(the ca-certificates package). Minimal Debian, Ubuntu and Alpine images do not include curl or
the bundle: install them first, for example apt-get install curl ca-certificates or
apk add curl ca-certificates. Verifying a download also needs
cosign v3 (or v2.4 or newer) and,
for the provenance check, slsa-verifier;
neither is part of a distribution's default install.
From source, with the Rust toolchain installed (pin the release tag you want):
cargo install --locked --git https://github.com/Hodeitek/hodeishield-cli --tag v0.5.1 hodeishield-clibrew install hodeitek/hodeishield/hodeishield # macOS and Linuxscoop bucket add hodeishield https://github.com/Hodeitek/scoop-hodeishield
scoop install hodeishield # Windows, per userScoop itself must be installed first (see scoop.sh); on a clean Windows machine
scoop is not a known command. scoop bucket add needs Git: on a fresh Scoop, run scoop install git first. Scoop's own installer
refuses an elevated (administrator) shell unless it is run with -RunAsAdmin; installing per user,
from a normal shell, is the recommended way and needs no administrator rights.
Homebrew asks you to trust a formula from a tap other than its own before it loads it
(Tap Trust). The full name above adds the tap and trusts only
this formula. If you added the tap before and see "Refusing to load formula … from untrusted
tap", run brew trust --formula hodeitek/hodeishield/hodeishield once. There is no need to trust
the whole tap or to turn tap trust off.
The Homebrew tap and the
Scoop bucket are updated from each release's own
files and SHA256SUMS when it is published, and both check those hashes when installing. Homebrew
also installs the man pages and shell completions. Scoop picks the arm64 build on Windows on Arm
automatically. A winget package has been submitted and is waiting for review by the winget community
repository; it is not available yet.
From 0.3.0, each release also publishes .deb and .rpm packages for x86_64 and arm64. They install /usr/bin/hodeishield, the man pages and the bash, zsh and fish completions.
From 0.4.2 they also install the user information in
/usr/share/doc/hodeishield/USER-INFORMATION.md.
Verify the package as in the next section (it has its own .sigstore.json and is listed in
SHA256SUMS), then:
sudo apt install ./hodeishield_<version>-1_amd64.deb # Debian, Ubuntu (arm64: _arm64.deb)
sudo dnf install ./hodeishield-<version>-1.x86_64.rpm # Fedora, RHEL (arm64: .aarch64.rpm)Upgrading is the same command with the newer package; sudo apt remove hodeishield or
sudo dnf remove hodeishield uninstalls it.
Nothing else is installed on the machine, so remove the files you copied. install.sh puts the
binary in /usr/local/bin or ~/.local/bin (or the --dir you gave) and the man pages in
share/man/man1 next to it:
hodeishield logout # revokes the sign-in and removes the stored token
rm -f "$(hodeishield config path)" # optional: the settings
rm "$(command -v hodeishield)" # the binary
rm -f /usr/local/share/man/man1/hodeishield*.1 # man pages (~/.local/share/man/man1 for ~/.local/bin)Run hodeishield logout and config path first; with the binary gone neither can run. If you
installed completions from hodeishield completions, remove the file where you saved it, usually
~/.local/share/bash-completion/completions/hodeishield (bash), _hodeishield in a directory of
your fpath (zsh) or ~/.config/fish/completions/hodeishield.fish (fish).
On Windows, hodeishield logout first removes the sign-in from Credential Manager, then:
- Zip: delete the folder you unpacked it into, and remove it from your
PATHif you added it (the zip never changesPATH). - MSI: uninstall from Settings → Apps or with
msiexec /x. This removes the program folder, thePATHentry and the Apps entry. - Scoop:
scoop uninstall hodeishield, which removes the program and its shim.
None of these remove the per-user settings in %APPDATA%\Hodeitek\HodeiShield\config, one folder
in each user profile that ran config set; delete it if you want them gone.
From 0.3.0, each release also publishes
hodeishield-<version>-x86_64-pc-windows-msvc.msi, signed with Authenticode like hodeishield.exe.
It installs for all users in %ProgramFiles%\HodeiShield CLI and adds that folder to the system
PATH (open a new terminal afterwards). A newer MSI upgrades an older one in place; uninstall from
Settings → Apps or with msiexec /x.
Silent installation, for Intune, Group Policy or any other deployment tool:
$p = Start-Process msiexec -Wait -PassThru -ArgumentList '/i hodeishield-<version>-x86_64-pc-windows-msvc.msi /qn /norestart /l*v hodeishield-install.log'
$p.ExitCode # 0 = installed, 3010 = installed and a restart is pending
# uninstall
$p = Start-Process msiexec -Wait -PassThru -ArgumentList '/x hodeishield-<version>-x86_64-pc-windows-msvc.msi /qn /norestart /l*v hodeishield-uninstall.log'
$p.ExitCodeRun msiexec through Start-Process -Wait -PassThru in PowerShell. Called directly, as
msiexec /i ..., it returns at once while the installer keeps running, and $LASTEXITCODE is not
updated (it keeps the value of an earlier command), so a script would carry on before the install
is done and could not tell whether it worked. From cmd.exe, use start /wait msiexec /i .... Deployment tools such as Intune and Group Policy
run the installer themselves and wait for it as usual.
- Intune: add it as a Line-of-business app (MSI), or as a Windows app (Win32) whose install
command is the plain command line
msiexec /i hodeishield-<version>-x86_64-pc-windows-msvc.msi /qn /norestart(and the same with/xto uninstall), not the PowerShell snippet above. Intune reads the product code from the MSI for detection. - Group Policy: Computer Configuration → Policies → Software Settings → Software installation, assign the MSI from a network share that the computers can read.
- Every version shares the upgrade code
{32B34E35-7A3F-422A-AC44-59204CD0D59D}, which detection rules and inventory tools can use to find any installed version.
From 0.3.0, each release also publishes install.sh, signed like the archives. It
downloads the archive for your platform, checks it against SHA256SUMS and its Sigstore signature,
and installs hodeishield (and its man pages) in /usr/local/bin when writable, otherwise
~/.local/bin. Do not pipe it into a shell: download it, verify it, read it, then run it.
v=<version> # e.g. the latest release, without the leading v
curl -fsSLO https://github.com/Hodeitek/hodeishield-cli/releases/download/v$v/install.sh
curl -fsSLO https://github.com/Hodeitek/hodeishield-cli/releases/download/v$v/install.sh.sigstore.json
cosign verify-blob --bundle install.sh.sigstore.json \
--certificate-identity https://github.com/Hodeitek/hodeishield-cli/.github/workflows/release.yml@refs/tags/v$v \
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
--certificate-github-workflow-trigger push install.sh
less install.sh
sh install.sh --version $vIt needs cosign v3, or v2.4 or newer, to check the archive's signature, and stops if it is missing
or older; --no-verify skips that check (the checksum still runs). To check install.sh itself with
cosign v2 as above, add --new-bundle-format to cosign verify-blob. sh install.sh --help lists
the options (--dir, --no-man).
From 0.4.0, each stable release also publishes a multi-architecture (linux/amd64, linux/arm64)
image at ghcr.io/hodeitek/hodeishield-cli, for CI jobs and container pipelines. It holds only the
release's own hodeishield binary on a
distroless static base (no shell, no package
manager, CA certificates included) and runs as a non-root user. There is no latest tag: use the
exact version, or better its digest. The image carries no documentation: the
user information for a version is in that release's tag, and, from
0.4.2, as USER-INFORMATION.md in its archives.
v=0.5.1 # the version you want
docker pull ghcr.io/hodeitek/hodeishield-cli:$v
docker run --rm -e HODEISHIELD_API_KEY ghcr.io/hodeitek/hodeishield-cli:$v vendors listHODEISHIELD_API_KEY is passed through from your environment (-e NAME without a value) so the key
never appears on the command line. hodeishield login does not work in a container (there is no
browser and no system keychain): use a tenant API key, see
Servers, containers and remote sessions.
The image is signed by digest with cosign (keyless) and carries the release's CycloneDX SBOM as a signed attestation. Verify both before you rely on the image:
v=0.5.1
image=ghcr.io/hodeitek/hodeishield-cli:$v
cosign verify $image \
--certificate-identity https://github.com/Hodeitek/hodeishield-cli/.github/workflows/container-image.yml@refs/tags/v$v \
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
--certificate-github-workflow-trigger release
cosign verify-attestation --type cyclonedx $image \
--certificate-identity https://github.com/Hodeitek/hodeishield-cli/.github/workflows/container-image.yml@refs/tags/v$v \
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
--certificate-github-workflow-trigger releaseBoth commands print the digest they verified; pin that digest
(ghcr.io/hodeitek/hodeishield-cli@sha256:...) in your pipeline, since a tag can be moved and a
digest cannot. The binary in the image is the one in the release's Linux musl archives, checked
against the release's signed SHA256SUMS before the image is built. The image's own base image is
pinned by digest in packaging/container/Dockerfile.
From 0.2.0, the Linux and macOS archives include a man page for every command in man/. To read
them with man, copy them into a directory on your man path, for example:
sudo mkdir -p /usr/local/share/man/man1
sudo cp hodeishield-<version>-<target>/man/*.1 /usr/local/share/man/man1/
man hodeishield-vendors-listEach release has a SHA256SUMS file, a Sigstore bundle (*.sigstore.json) for every archive and for
SHA256SUMS, and SLSA provenance (hodeishield.intoto.jsonl). In short:
grep hodeishield-0.5.1-x86_64-unknown-linux-musl.tar.gz SHA256SUMS | sha256sum -c -
cosign verify-blob hodeishield-0.5.1-x86_64-unknown-linux-musl.tar.gz \
--bundle hodeishield-0.5.1-x86_64-unknown-linux-musl.tar.gz.sigstore.json \
--certificate-identity https://github.com/Hodeitek/hodeishield-cli/.github/workflows/release.yml@refs/tags/v0.5.1 \
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
--certificate-github-workflow-trigger pushOn Windows, PowerShell has no sha256sum: compare the hash of the zip or MSI with its line in
SHA256SUMS. cosign for Windows is a cosign-windows-amd64.exe download on its
release page (winget install sigstore.cosign may
fail with a missing-source error), and the same verify-blob command works with Windows paths:
(Get-FileHash .\hodeishield-0.5.1-x86_64-pc-windows-msvc.zip -Algorithm SHA256).Hash.ToLower()
Select-String hodeishield-0.5.1-x86_64-pc-windows-msvc.zip SHA256SUMS # the two hashes must match
# the same two lines with .msi for the MSI
cosign verify-blob .\hodeishield-0.5.1-x86_64-pc-windows-msvc.zip `
--bundle .\hodeishield-0.5.1-x86_64-pc-windows-msvc.zip.sigstore.json `
--certificate-identity https://github.com/Hodeitek/hodeishield-cli/.github/workflows/release.yml@refs/tags/v0.5.1 `
--certificate-oidc-issuer https://token.actions.githubusercontent.com `
--certificate-github-workflow-trigger pushA valid signature proves the file was built by this repository's release workflow from exactly that
tag, with no long-lived key involved; put your version in the identity rather than matching any tag. The full steps, including the provenance check with slsa-verifier, are in
docs/verifying-releases.md.
From 0.2.0, hodeishield.exe is signed with Authenticode, so Windows accepts it;
docs/verifying-releases.md shows how to check the signature. The macOS
binary is not yet signed with an Apple Developer ID or notarized; that comes in a later release
(#50). Until then, and for Windows versions
before 0.2.0, a copy downloaded with a browser can be blocked or flagged. The cosign check above is
the proof that the file is genuine; once it passes, unblock the binary as follows.
macOS. Gatekeeper may refuse to open hodeishield ("Apple could not verify… is free of
malware"). Verify the download first, then allow the binary in System Settings: after the first
blocked attempt, open System Settings → Privacy & Security, scroll down to the message about
hodeishield, click Open Anyway, confirm, and run the command again. The dialog's wording and
button labels follow the system language, so they may differ from the English text quoted here.
Or remove the quarantine attribute from the unpacked binary:
xattr -d com.apple.quarantine ./hodeishield # "No such xattr" means there was nothing to removeA browser download sets that attribute, and unpacking the archive with tar in Terminal keeps it
on the binary, so the block still applies. A download with curl does not set it in the first
place:
curl -LO https://github.com/Hodeitek/hodeishield-cli/releases/download/v0.5.1/hodeishield-0.5.1-universal-apple-darwin.tar.gzWindows. SmartScreen can warn about a signed file that is still new to it, and versions before 0.2.0 are not signed at all. To avoid the warning, unblock the archive before unpacking it, so the executable does not inherit the download mark:
Unblock-File .\hodeishield-<version>-x86_64-pc-windows-msvc.zip
Expand-Archive .\hodeishield-<version>-x86_64-pc-windows-msvc.zipIf you already unpacked it, run Unblock-File on hodeishield.exe instead. Windows PowerShell
5.1's Expand-Archive does not copy the download mark to the unpacked files, so unblocking matters
when you unpack with File Explorer's "Extract All" or another tool that carries the mark over.
If SmartScreen still shows "Windows protected your PC", choose More info → Run anyway («Más
información → Ejecutar de todos modos» on a Spanish system).
Create a key in the app under Settings → API keys, with only the read scopes you need, and export it:
export HODEISHIELD_API_KEY=hsk_...
hodeishield whoamiWhen HODEISHIELD_API_KEY is set it is used for every call, whatever else is configured. Keep it in
your CI's secret store; the CLI never prints it.
whoami asks the API who the credential is and shows the tenant it reads (name and id), its
organisation and, for a key, its scopes; for a sign-in, also the signed-in user when the service
provides it; for a portfolio key, the MSSP and how many client tenants it
can select. With --json these are the tenant, organization, portfolio and
credential_scopes fields. If the API cannot be reached, whoami still shows the local settings,
leaves those fields null and says why on standard error.
hodeishield login # opens the browser; the app redirects back to 127.0.0.1
hodeishield login --device # no browser here: approve a short code from another device
hodeishield login --no-browser # print the sign-in URL instead of opening a browser
hodeishield whoami
hodeishield logout # revokes the token at the app and removes it from the keychainYou sign in to app.hodeishield.com as usual (2FA, passkey or SSO), and on the consent screen you choose the one tenant the CLI may read. A sign-in token never covers more than that tenant, and it only reads. To use an unrelated tenant, define another profile and sign in to it:
hodeishield config set --profile client-b api_url https://api.hodeishield.com
hodeishield login --profile client-bA profile that has not been defined is refused (Profile ... is not defined).
Two profiles signed in as the same user, to the same tenant, with the same flow (browser or
--device) share one sign-in at the app: logging out of either one signs out both. Profiles on
different tenants, or one signed in with the browser and the other with --device, are independent.
login first checks that the selected token store is available and stops before opening the browser
or showing a code if it is not. On a machine without a keychain, see
Servers, containers and remote sessions.
An MSSP reads its clients with one credential instead: see Several tenants (MSSP).
With --device, open the address the CLI prints on any device and type the code by hand: the app
does not accept a link with the code filled in, on purpose.
The CLI asks for the read scopes its commands need (vendors, alerts and risks; every compliance
framework the app offers; evidence; endpoints; reports) and for a refresh token. To ask for fewer, set them:
hodeishield config set oauth_scopes "supply_risk:read offline_access".
The token is kept in the system keychain (macOS Keychain, Windows Credential Manager, or the Secret Service — GNOME Keyring, KWallet — on Linux) and refreshed automatically, also when the app ends an access token early. Without a keychain, signing in is refused rather than falling back to a file; the file store below is an explicit choice, never a fallback.
The keychain can also be out of reach in a session that has one on the machine, such as an SSH or
other remote logon on Windows. With no API key set, commands then stop with exit code 3 and say that
the system keychain is not available in this session; set HODEISHIELD_API_KEY in remote or
automated sessions. login and logout exit 1 in that case, not 3. With the key set, the keychain
is not read at all.
To cut the CLI's access from elsewhere, revoke it in the app under Account → Applications. Signing out of the web app does not cut it.
Where there is no desktop session (an SSH server, a container, WSL without a desktop, a CI runner)
there is usually no keychain either, and hodeishield login stops before it opens a browser or shows
a code. You have two options, in this order of preference:
-
A tenant API key (see With a tenant API key). It works everywhere, needs no storage on the machine, can be limited to the read scopes you need and is the right choice for containers and CI. Do not sign in during
docker build, and never commit or publish a container in which someone has signed in. -
A plain-text token file, if you want to sign in as yourself on a machine you control, such as a jump host. You opt in explicitly, and the trade-off is that the tokens are stored unencrypted in a file that only your user can read. Anyone who can read your files, such as
root, a backup, a snapshot or a copy of your home directory, can use the refresh token:- How long. At most 90 days from the sign-in, however often the session is refreshed, and
at most 30 days after the CLI last refreshed it. It ends earlier with
hodeishield logout, when you change or reset your password, when an administrator revokes it, when you are removed from the tenant, or when you revoke the access in the app under Account → Applications. - Reuse is detected. Each refresh replaces the refresh token. If someone refreshes with a copy of your file, the next refresh by your own CLI is seen as a reuse of an old token and ends the session for both. An unexpected "sign in again" is a reason to revoke the access in the app and check who could read the file.
export HODEISHIELD_TOKEN_STORE=plaintext-file # or: hodeishield config set token_store plaintext-file hodeishield login --device hodeishield whoami hodeishield logout # revokes the tokens, then deletes the file
- Where. One file per profile in the sessions directory of your state directory:
$XDG_STATE_HOME/hodeishield/sessions/(by default~/.local/state/hodeishield/sessions/) on Linux, and~/Library/Application Support/com.Hodeitek.HodeiShield.sessions/on macOS (next to, not inside, the folder that holds the configuration file). The configuration file never holds a token, and the file names are a hash of the profile name, not the name itself (a hash of a guessable name can be reversed, so do not treat it as hiding the profile). The parents of that directory, such as your state directory, must not be writable by other users. - Checks. The directory is created with mode 700 and the files with mode 600, written by
replacing the file atomically. On every read and write the CLI refuses, and does not repair, a
file or directory that is a symbolic link, belongs to another user, or is open to group or
others. Commands that need a credential then stop with exit code 3 (
loginandlogout: 1) and tell you what is wrong; fix it yourself (chmod), and revoke the tokens in the app if someone else could have read them. Temporary files left by a write that was interrupted are removed the next time that profile is saved or signed out. - Do not run it as
rootor withsudofor your user. A run asrootthat uses your home directory can create root-owned directories there, which your own runs then refuse (androotrefuses yours, since they belong to another user). Sign in with the user that will run the commands. - No fallback. The setting must be the same on every command, and the CLI never switches
between the keychain and the file on its own. Tokens are not moved when you change it: sign out,
change the setting, sign in again. The values are
plaintext-fileandkeychain(the default); anything else is an error. - Network home directories. Do not keep the state directory on NFS or another shared file
system: the file would be readable by the server's administrators and by
rooton other machines that mount it, and its permissions are not enforced the same way. On Linux the CLI warns onloginwhen the directory is on NFS or SMB; it does not stop you. PointXDG_STATE_HOMEat a local directory instead. - Deleting is not revoking.
logoutrevokes the tokens at the app and then deletes the file, but removing the file by hand, or restoring it from a backup, does not end the session. Only revoking does. - Not on Windows yet. Windows has no permission bits to check, so selecting the file store there is refused with exit code 1, before anything is asked of the app.
- How long. At most 90 days from the sign-in, however often the session is refreshed, and
at most 30 days after the CLI last refreshed it. It ends earlier with
hodeishield vendors list --criticality critical
hodeishield vendors get 0b8f2a52-6a0e-4d5c-9d3e-1f2a3b4c5d6e
hodeishield alerts list --status open --since 2026-01-01T00:00:00Z
hodeishield risks list --min-inherent-score 15 --sort inherent_score
hodeishield compliance posture nis2
hodeishield compliance controls --framework nis2 --status failing
hodeishield evidence list --expiring-before 2026-12-31T00:00:00Z
hodeishield endpoints list --status active
hodeishield reports list --type monthly --status completed$ hodeishield vendors list --criticality critical
ID NAME DOMAIN CRITICALITY ACTIVE LAST SCAN
0b8f2a52-6a0e-4d5c-9d3e-1f2a3b4c5d6e Example Hosting hosting.example.com critical yes 2026-09-27 06:12Z
5c1d9e0a-3b7f-4a2e-8c6d-2e4f6a8b0c1d Sample Payments pay.example.net critical yes 2026-09-26 22:40ZLists show one page (50 items by default); --page, --per-page (up to 200) and --all walk the
rest. --all stops with an error past 100,000 items (or 1,000 pages); narrow the list with filters
or walk it with --page. Every list takes --sort and --order; --help on any command lists
what it accepts.
reports list and reports get show the reports generated for the tenant; reports download
saves a completed report's file:
$ hodeishield reports list --status completed
ID TYPE TITLE PERIOD STATUS CREATED
7d3e1f2a-4b5c-4d6e-8f70-81a2b3c4d5e6 monthly September summary 2026-09-01 to 2026-09-30 completed 2026-10-01 06:00Z
$ hodeishield reports get 7d3e1f2a-4b5c-4d6e-8f70-81a2b3c4d5e6 # includes download_formats
$ hodeishield reports download 7d3e1f2a-4b5c-4d6e-8f70-81a2b3c4d5e6 --format xlsx --locale es
Saved monthly-2026-09.xlsx (48213 bytes).--type(also--report-type) and--statuslist the values the API publishes in--help; like the other listed filters, a value that is not listed gets a warning and is still sent as typed.--formatispdf(the default) orxlsx, and--localeone ofes,en(the default),de,fr,itandeu.reports getlists the formats a report can be downloaded in.- Without
--output, the file is saved in the current directory under the name the API suggests, reduced to a plain stem: only its last part is kept, every character other than a letter, a digit,.,_and-becomes_, leading dots are dropped and everything from the first.on is cut. The extension is always that of the format (.pdfor.xlsx), never the one the API suggested, soconftest.pyis saved asconftest.pdf. When nothing usable is left, the name isreport-<id>.<format>. The name is never used as a path. --output <PATH>saves it there instead; a directory saves it there under the suggested name.--output -writes the file to standard output, and is refused (exit code 2) when standard output is a terminal.- An existing file is never replaced unless
--forceis given. The file is written next to its destination and then put in place, so the path never holds part of a file. On Unix, the saved file is readable only by you (mode 0600);--forcekeeps the permissions of the file it replaces. The directory is checked before the download starts. - The note
Saved <path> (<size>).goes to standard error. With--json, standard output holds{"path": ..., "bytes": ..., "content_type": ...}instead, and there is no note. - A report that is still being generated, that failed, that has no file in that format, or whose file is larger than the API serves (20 MiB) fails with exit code 1 and says which. Each download is recorded in the tenant's audit trail.
An MSSP reads all of its client tenants with one credential: a portfolio key, created in the app by an organisation administrator of an active MSSP organisation, in the MSSP area (API keys). Nobody else can create one: not a tenant administrator, and not a platform operator acting outside an MSSP organisation. You do not need a profile, a key or a sign-in per client, and you should not set them up that way.
A portfolio key reads only, always expires (90 days by default, 365 at most), and works on the
HodeiShield API only. It can be limited when it is created to some of your clients and to source IP
ranges; a request from another address is refused like an unknown key (exit code 3). Signing in
with hodeishield login does not cover a whole portfolio: use the key. Rotate your portfolio keys
before they expire, so scripts do not stop working; a new key and its predecessor both work for up
to 7 days while the old one is in force.
1. Use the credential as any other key.
export HODEISHIELD_API_KEY=hsp_3f2a9c0d1b4e_... # the portfolio key; keep it in your CI's secret storeThe key starts with hsp_, a 12-character identifier and _, then a long secret. The first 16
characters identify the key in the app and in audit entries; the CLI never prints any part of the
key. The same variable takes a tenant API key as before.
2. List the client tenants it may read.
$ hodeishield tenants list
SLUG NAME ORGANISATION TYPE STATUS LICENCE ID
acme Acme S.L. Acme Group client active in_force 11111111-1111-4111-8111-111111111111
beta Beta Corp Beta Group client active grace 22222222-2222-4222-8222-222222222222It takes --json, --csv, --page and --all like any list, sorted by name. It includes your
own organisation's tenants (TYPE own for the internal one) and clients that are inactive or have
no licence in force, which the STATUS and LICENCE columns tell apart. A key that was limited to
some tenants when it was created lists and reads only those. The list is read from the API each
time: a client added to or removed from your portfolio shows up on the next command.
3. Select the client a read command reads. The selection goes with the command and is never remembered, so a later command cannot read another client by surprise.
hodeishield --tenant acme vendors list # one client, by slug or id
hodeishield --all-tenants alerts list --status open--tenant <slug|id>works on every read command (list,get,compliance posture,reports download). The CLI printsClient tenant: acme (<id>)on standard error, so a log shows which client was read; standard output is exactly what it is without a selection.--all-tenantsworks onlistcommands andcompliance controls. It fetches every page of each client (--per-pageapplies,--pagedoes not) and adds the tenant to each row: aTENANTcolumn in the table, andtenant_id,tenant_slugandtenant_namefields in--jsonand--csv. Clients that are inactive, or whose licence status isinactive, are skipped with a note on standard error. A client in its grace period is read. A client whose licence status this CLI does not know is not read either; it is named on standard error (licence status … is not known to this version; not read) and the command exits non-zero, so nothing goes missing silently. Clients are asked one after the other. In the table, a slug with characters other thana-z,0-9and-is followed by the start of the client's id, so look-alike names can be told apart;--jsonand--csvcarry the full values.- The tenant on a row is the one whose own request returned it. The CLI asks the API about one client at a time, and refuses an answer that does not confirm that client.
hodeishield --all-tenants alerts list --status open --json \
| jq -r '.[] | [.tenant_slug, .severity, .title] | @tsv'
hodeishield --all-tenants vendors list --criticality critical --csv > critical-vendors.csv4. When something is not readable.
- A client that is not in your portfolio (or that you mistyped) fails with
No client tenant … in this credential's portfolio.and exit code 5, before anything is read. - With
--all-tenants, a client that cannot be read on its own account (for example its licence is not in force, exit code 8) is reported on standard error and skipped; the command prints the others and still exits non-zero, so a script does not mistake a partial result for a complete one. This is the only signal of a partial result: check the exit code, and read standard error for which clients were left out. A client named by both an id and a slug of another client (a slug can look like an id) is refused as ambiguous; use the id shown bytenants list. - Without
--tenantor--all-tenants, a portfolio key is refused by the API, and the CLI tells you to choose. A single-tenant key or sign-in behaves exactly as before; asking it for a client fails with exit code 4 (it lacks the portfolio scope). - A client you named that is deactivated, or whose licence is not in force, fails with exit code 8.
- A portfolio key is limited to 100 requests per minute, and all the portfolio keys of your
organisation together to 600 (the API may change these numbers); a client tenant's own limits
can be lower. The CLI waits out a short
Retry-Afterand retries; if the wait is longer, or the limit is still exhausted, the command stops with exit code 6 and says which limit it hit (portfolio_key,portfolio_org,portfolio_tenantortenant), instead of asking the next client. Use--per-page 200and narrow with filters to need fewer requests. - Against an API that does not offer portfolio access yet,
tenants list,--tenantand--all-tenantsfail withThis API does not offer portfolio access yet.and exit code 7. Nothing else changes.
5. Revoke access. Revoke the portfolio key in the app, in the MSSP area; it stops working on its next request. A client leaving your portfolio is dropped from the list on the next command.
--json prints the body the API returned, unchanged, including fields newer than your CLI. With
--all, it prints a single JSON array of every item:
hodeishield alerts list --status open --all --json | jq -r '.[] | [.severity, .title] | @tsv'--csv prints the items of any list command, and of compliance controls, as CSV instead of a
table. It works with --all, and not with --json:
hodeishield vendors list --all --csv > vendors.csv- The columns are the fields
--jsonshows for each item, in the API's order. A nested value (a list or an object) is written as compact JSON. - The file follows RFC 4180: a header row, fields quoted when needed, CRLF line ends. It is UTF-8 without a byte order mark, which suits scripts; to open it in Excel, use Data → From Text/CSV and choose the file origin "65001: Unicode (UTF-8)".
- A text cell that starts with
=,+,-,@, a tab or a carriage return gets a leading', so a spreadsheet shows it as text instead of running it as a formula. Numbers are left as they are, so-5stays a number. - No items print nothing on standard output; the count and paging notes go to standard error.
| Code | Meaning |
|---|---|
| 0 | Success |
| 1 | Error (invalid input, configuration, unexpected answer) |
| 2 | Invalid command line |
| 3 | Not authenticated: no credential (also when the system keychain cannot be reached in this session, or the opt-in token file is refused as unsafe; login and logout exit 1 for that), or the API rejected it |
| 4 | The credential lacks the scope the command needs |
| 5 | Not found (or not in your organisation) |
| 6 | Rate limit exhausted (the CLI already retried short waits) |
| 7 | The API could not be reached or failed, or does not offer portfolio access yet |
| 8 | The tenant's licence is not in force: a tenant administrator must renew or reactivate it |
Errors go to stderr with a hint and, when the API gave one, a request id: quote it when you
contact support. --verbose logs each request's method, URL, status and request id to stderr, never
a credential.
The CLI follows Semantic Versioning. Its scripting surface is what a script can depend on:
- the command names and flags;
- the field names and types in
--jsonand--csvoutput; - the exit codes 0 to 8 and what each one means (see Exit codes);
- what goes to standard output (the data) and to standard error (notes and errors).
While the version is 0.x, a change that breaks the scripting surface only ships in a minor version (0.2 to 0.3, never 0.2.0 to 0.2.1), and the changelog lists it under Changed (scripting surface), with what to do about it. Read that heading before you upgrade. Patch releases fix bugs and do not break the scripting surface. A 1.0 release will be announced in the changelog.
Adding to the scripting surface is not a breaking change: new commands and flags, and new fields in
--json output. Giving an existing case a different exit code is a breaking change, even when the
code is new (0.2.0 moved an inactive licence from 4 to 8). Write scripts to ignore the fields they
do not know: --json prints the body the API returned, so it can gain fields at any time.
The table output and the messages on standard error are for people and may change in any release.
Scripts should use --json or --csv for data, and the exit code for what happened, and should not
parse tables or messages.
Settings come from, in order: flags, environment variables, the profile in the configuration file,
and the defaults (https://api.hodeishield.com, https://app.hodeishield.com).
hodeishield config show # the settings in effect
hodeishield config path # where the file is
hodeishield --profile staging config set api_url https://api.example.test
hodeishield config use staging # make it the default profile
hodeishield config set update_check false # no new-version notice (global, not per profile)
hodeishield config set token_store plaintext-file # opt-in token file, see the section on servers
hodeishield --profile default vendors list # or pick one per command| Variable | Purpose |
|---|---|
HODEISHIELD_API_KEY |
Tenant API key; takes precedence over a sign-in |
HODEISHIELD_PROFILE |
Profile to use |
HODEISHIELD_API_URL |
API base URL |
HODEISHIELD_APP_URL |
App base URL (sign-in) |
HODEISHIELD_CONFIG |
Path of the configuration file |
HODEISHIELD_TOKEN_STORE |
Where sign-in tokens are kept: keychain (the default) or plaintext-file, an opt-in; takes precedence over token_store in the configuration file |
HODEISHIELD_NO_UPDATE_CHECK |
Any non-empty value turns off the new-version notice |
config set and config unset take api_url, app_url, oauth_client_id and oauth_scopes
(per profile), and update_check (true or false) and token_store (keychain or
plaintext-file), which are global.
The configuration file holds URLs and sign-in settings only, never a credential. update_check and
token_store are global settings, not per profile. An older CLI refuses a configuration file that
contains token_store; use the environment variable there. URLs must use HTTPS;
plain HTTP is accepted only for localhost.
On an interactive terminal the CLI tells you, in one line on standard error, when a newer release exists:
A newer version of hodeishield is available: 0.4.0 (you have 0.3.0). See https://github.com/Hodeitek/hodeishield-cli/releases
It never updates anything. Privacy, in full:
- What is sent. One unauthenticated
GETto GitHub's public API,https://api.github.com/repos/Hodeitek/hodeishield-cli/releases/latest, with a 2 second limit. Only what HTTP requires goes with it, includingUser-Agent: hodeishield-cli/<version>andAccept. No API key, token, profile, tenant, command or machine identifier is sent, and nothing goes to HodeiShield: GitHub sees your IP address, as it does for any request. Your proxy settings apply (see Networking). - When. At most once every 24 hours, after the command has finished and printed its output.
The time of the last attempt, and the version it found, are kept in
update-check.jsonnext to the configuration file (seehodeishield config path). A failed attempt (offline, timeout, GitHub unavailable) is silent, is not repeated before the next day, and never changes a command's output or exit code. Add--verboseto see why nothing was shown. - Where it is silent. When standard error is not a terminal, with
--jsonand--csv, forcompletions, and whenever theCIenvironment variable is set, to any value. The notice goes to standard error, so a terminal on standard error is what decides, not whether standard output is piped. - Turning it off.
export HODEISHIELD_NO_UPDATE_CHECK=1, or once for all profileshodeishield config set update_check false(config unset update_checkrestores the default). With either, no request is made.
Shell completions: hodeishield completions bash|zsh|fish|powershell|elvish.
The CLI reaches api.hodeishield.com and app.hodeishield.com over HTTPS (port 443). Behind a
corporate proxy, nothing needs configuring in the CLI itself; it follows the proxy settings of the
machine.
-
Proxy variables.
HTTPS_PROXY,HTTP_PROXY,ALL_PROXYandNO_PROXYare honoured, and so are their lower-case forms. Since all traffic is HTTPS,HTTPS_PROXY(orALL_PROXY) is the one that matters; useNO_PROXYfor hosts that must be reached directly.export HTTPS_PROXY=http://proxy.example.com:3128 export NO_PROXY=localhost,127.0.0.1 hodeishield vendors list
-
Operating system settings. On macOS and Windows the system proxy settings are read as well, next to the variables. On Linux only the variables are read.
-
Certificates. The CLI trusts the operating system's certificate store, not a bundled list. If your organisation inspects TLS and its CA is installed in that store (the usual case on managed Windows and macOS machines, or after
update-ca-certificates/update-ca-truston Linux), the CLI trusts the proxy's certificates with no further setup. On Linux you can also pointSSL_CERT_FILE(a PEM file) orSSL_CERT_DIRat your CA certificates. -
What cannot be configured. There is no CLI option to add a CA or to skip certificate verification, and
SSL_CERT_FILE/SSL_CERT_DIRhave no effect on macOS and Windows: add the CA to the system store there. -
Sign-in on this machine.
hodeishield loginwaits for the browser on127.0.0.1; that redirect is made by your browser to the CLI, not by the CLI, so the CLI's proxy settings do not apply to it (the CLI also never uses a proxy when its API or app URL is a loopback address). Your browser's own proxy settings apply to the sign-in page. Where the browser cannot reach the machine, usehodeishield login --device.
To diagnose, add --verbose: each API request is logged to standard error with its URL, status,
time and request id, followed by each retry and its reason. A failure to connect, including a proxy
that refuses the connection or a certificate that is not trusted, ends with exit code 7 and a
message that starts with Could not reach the API: (or Could not reach the app during sign-in)
followed by the cause. A certificate error there usually means the proxy's CA is missing from the
store described above.
This behaviour comes from the CLI's HTTP library. It is verified in CI on Linux, macOS and Windows against a TLS-inspecting proxy whose CA is installed in the system store.
crates/hodeishield-cli— thehodeishieldbinary.crates/hodeishield-api— a Rust client for/v1, generated from the API's published OpenAPI document vendored inopenapi/v1.json. It ignores fields it does not know, so it keeps working as/v1grows.xtask— the generator (cargo xtask codegen, and--checkin CI).
See CONTRIBUTING.md and SECURITY.md. To report a vulnerability, use GitHub private reporting or email security@hodeitek.com, never a public issue. Security advisories are published under Security → Advisories.
From 1.0.0, each major version is supported with security fixes for five years; during 0.x, fixes go
into the latest 0.x release (Supported versions).
docs/user-information.md gathers the manufacturer's contact details, the
support period, secure use, how to update and remove the CLI, and where the SBOM is published.
Releases from 0.4.2 ship a copy as USER-INFORMATION.md in their archives and packages.
docs/technical-documentation.md is the index of the technical
documentation required by the Cyber Resilience Act.
Licensed under the Apache License, Version 2.0. Copyright 2026 Hodeitek S.L.; see NOTICE.
HodeiShield® and Hodeitek® are registered trademarks of Hodeitek S.L.; the license grants no rights to them (Apache-2.0 §6).
HodeiShield is in beta. To request access, visit https://hodeishield.com/en/beta and review the beta terms of service.
- HodeiShield: the third-party risk, supply-chain security and compliance platform that this CLI reads from; sign in at app.hodeishield.com and create API keys there.
- Hodeitek: the company that builds HodeiShield and maintains this CLI.
