Docs

Get id9s running

The essentials to install id9s, configure a context and start managing identities.

Install

On macOS and Linux, install with Homebrew:

brew install lazy-admins/id9s/id9s

This adds the lazy-admins/id9s tap and installs the formula in one step. Update later with brew upgrade id9s; check the installed version with id9s --version.

Without Homebrew (and on Windows), download the archive for your platform from GitHub Releases, check it against checksums.txt, and put the id9s binary somewhere on your PATH. Builds are available for macOS (Apple Silicon and Intel), Linux (amd64 and arm64) and Windows (amd64).

The binaries aren't signed yet. On macOS, a binary downloaded through a browser is quarantined by Gatekeeper: clear the flag once with xattr -d com.apple.quarantine ./id9s. Homebrew installs aren't affected. On Windows, SmartScreen may ask you to confirm the first launch.

id9s is free to use under the Apache License 2.0. Each archive includes LICENSE, NOTICE and THIRD_PARTY_LICENSES for the open source components it's built with.

Setup

id9s reads a single multi-context config file, ~/.id9s/config.yaml — kubeconfig-style, so you can define several AD domains and Entra tenants and switch between them without restarting.

You don't need to hand-write it first: launch id9s with no config at all and it prompts you to add a context interactively. If you'd rather start from a file:

current_context: prod-corp

contexts:
  - name: prod-corp
    type: ad              # optional, "ad" is the default
    domain_host: dc01.corp.local:636
    base_dn: DC=corp,DC=local
    bind_user: svc_id9s@corp.local

  - name: prod-entra
    type: entra
    tenant_id: 11111111-1111-1111-1111-111111111111
    client_id: 22222222-2222-2222-2222-222222222222

No password or token ever lives in this file. The AD bind password is asked once and saved to your OS keyring (Keychain / Credential Manager / Secret Service) on first use; Entra ID has no password at all — see below.

Entra ID context

Entra ID uses interactive browser SSO (OAuth2 Authorization Code + PKCE, public client) instead of a stored password. id9s doesn't ship with a shared app registration — your tenant admin creates one for id9s:

  1. Entra admin center → App registrations → New registration.
  2. Platform: Mobile and desktop applications. Redirect URI: http://localhost — exactly that, no port, no path.
  3. Enable "Allow public client flows". No client secret is needed or stored.
  4. Grant these delegated Graph permissions (with admin consent):
PermissionNeeded for
User.ReadWrite.AllRequired — core user management
GroupMember.ReadWrite.AllRequired — group membership editor
offline_access, openid, profileRequired — sign-in and token refresh
Organization.Read.AllOptional — readable license names instead of raw SKU codes
AuditLog.Read.AllOptional — recent sign-in history shown before kill sessions
UserAuthenticationMethod.ReadWrite.AllOptional — MFA methods and passkeys view, reset MFA, revoke passkey
Device.ReadWrite.AllOptional — registered devices, revoke device

In id9s: add a context, pick Entra ID, enter the Tenant ID and Client ID. Connecting opens your browser for sign-in; the refresh token is cached in the keyring and renewed silently afterward.

Adding a permission to an existing app registration doesn't retroactively apply to contexts already connected — a silent token refresh doesn't request new scopes. Press A on the context to force a fresh interactive login in place (no need to delete and re-add it).

Keybindings

The essentials from the :users view — every destructive one below requires a second, explicit confirmation step (see Security).

KeyActionScope
/Real-time filterBoth
SSort the list by any visible column (default: display name)Both
iShow inactive users (no sign-in for 30/60/90/180/365+ days, cycles on each press)Both
dUser details (groups, licenses, MFA methods, passkeys, devices, password last set, last sign-in, created)Both
eEnable / disable accountBoth
pReset passwordBoth
rRefresh the listBoth
mModify profile attributesBoth
gGroup membership editorBoth
uUnlock accountAD only
MMove OUAD only
kKill sessions (shows recent sign-ins first)Entra ID only
RReset MFAEntra ID only
PRevoke a single passkey (FIDO2 security key or passkey in Microsoft Authenticator)Entra ID only
vRevoke a registered deviceEntra ID only
lAssign / remove licensesEntra ID only
sSwitch context (no restart)Both
xManage contexts (add/edit/delete/clone)Both
LAudit log viewer, filterableBoth
ARe-authenticate (force a fresh browser login in place)Entra ID only
nOnboard user (guided account creation)Both
oOffboard user (checklist of retirement actions)Both
SpaceSelect / deselect the user for a bulk actionBoth
BBulk actions on the selected usersBoth
EscBack: closes the current popup, picker or details view; from the user list, returns to the context managerBoth
qQuit id9s (asks for confirmation)Both

The onboarding wizard (n) suggests a username from the account's first/last name (accent transliteration, e.g. Niccolò → niccolo). On an AD context it creates the account on-prem only — id9s doesn't control Entra Connect's sync schedule, so it never claims to also create a matching Entra ID object — with a target OU and optional initial groups. On an Entra ID context it creates the account directly via Graph, with usage location, optional initial groups, and optional initial license assignment. Confirmation requires typing the account's UPN, the same tier as k/R/v.

The offboarding wizard (o) retires an account through a checklist pre-checked from what's actually found for the user: disable the account and remove it from all groups on both backends; on Entra ID also kill sessions, reset MFA, revoke devices, and remove licenses; on AD, optionally move it to a target OU (never pre-checked). Steps run best-effort, so one failure doesn't block the rest, and each one is logged to the audit log. Confirmation requires typing the account's UPN.

Bulk actions (B) apply one action to every selected user: disable/enable and add to/remove from groups on both backends, unlock and move OU on AD, kill sessions, reset MFA and assign/remove licenses on Entra ID, plus offboarding on both. Users are selected one at a time with Space: there is deliberately no "select all", so a wrong or empty filter can never put the whole directory into a bulk action. Bulk offboarding uses the same checklist as o, and each step runs only for the users it applies to: an account that's already disabled still gets its groups, sessions and licenses cleaned up. The selection survives filter changes and is cleared when you switch context. Before anything runs, a review screen lists every selected user, including those hidden by the current filter, with exactly what will happen to each one; users already in the target state are marked as skipped. To confirm, type the random word shown for that run. On AD your password is re-confirmed first. Users are processed one at a time with visible progress, Esc stops after the current user, and every change is written to the audit log.

The user list shows a last sign-in column, and i narrows it to accounts with no sign-in for 30, 60, 90, 180 or 365+ days (each press moves to the next threshold, then turns the filter off). An account that never signed in only counts as inactive once it's older than the threshold, so new hires waiting for their first login aren't flagged. On AD the value comes from lastLogonTimestamp, which can lag by up to about 14 days. On Entra ID it comes from signInActivity (the last successful sign-in when the tenant reports it) and needs an Entra ID P1/P2 license plus AuditLog.Read.All; without them the column shows "Unknown" and the filter stays off. Entra ID only sees cloud sign-ins, so check synced users who only work on-premises from the AD context before offboarding them.

The list is sorted by display name by default; S sorts by any visible column, and choosing the sorted column again reverses it. Missing values always stay at the bottom. Company and department columns appear next to the display name when the terminal is wide enough.

If a domain controller closes an idle connection (Active Directory does this after 15 minutes by default), id9s reconnects automatically on the next action, with no need to switch context.

Actions with no equivalent on the other backend (e.g. u/M on Entra ID) are hidden entirely instead of being offered and failing.

Exchange Online mailbox management isn't available from id9s. Permissions/delegates (Full Access, Send As, Send on Behalf) have no Graph REST endpoint at all — only Exchange Online PowerShell. Even read-only mailbox settings were tried and dropped: the delegated Graph permission for it only reliably works for the signed-in operator's own mailbox, not other users', regardless of admin consent or directory role — reading arbitrary users' mailbox data needs an app-only permission, which requires a client secret and conflicts with id9s's no-stored-secret design.

Security & audit

Every destructive or irreversible action — offboarding, kill sessions, bulk actions, password resets — requires an explicit two-step confirmation: not a plain y/n, but typing the username or pressing a dedicated second confirmation key.

Every action performed is logged locally as structured JSON (default ~/.id9s/audit.log, configurable per context) before it's considered complete.

Credentials never touch disk in plaintext: AD bind passwords and Entra refresh tokens live only in your OS's native secret store (Keychain, Credential Manager, or Secret Service), accessed exclusively through id9s's own keyring layer.