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.
Active Directory context
Connects over LDAPS (port 636) by default using the fields above:
| Field | What it is |
|---|---|
domain_host | Domain controller FQDN or IP, with the LDAPS port (dc01.corp.local:636) |
base_dn | Search base for users, e.g. DC=corp,DC=local |
bind_user | Service account UPN used to bind, e.g. svc_id9s@corp.local |
plain_ldap | Optional, defaults to false. Set to true to connect over unencrypted LDAP on port 389 instead, for domain controllers without LDAPS configured |
The bind password is requested once on first connect and stored in the OS keyring — never in config.yaml, never in plaintext. LDAPS is the recommended default: switching to plain_ldap sends that password unencrypted over the network, so the interactive context form warns you and asks for a second confirmation before saving one.
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:
- Entra admin center → App registrations → New registration.
- Platform: Mobile and desktop applications. Redirect URI:
http://localhost— exactly that, no port, no path. - Enable "Allow public client flows". No client secret is needed or stored.
- Grant these delegated Graph permissions (with admin consent):
| Permission | Needed for |
|---|---|
User.ReadWrite.All | Required — core user management |
GroupMember.ReadWrite.All | Required — group membership editor |
offline_access, openid, profile | Required — sign-in and token refresh |
Organization.Read.All | Optional — readable license names instead of raw SKU codes |
AuditLog.Read.All | Optional — recent sign-in history shown before kill sessions |
UserAuthenticationMethod.ReadWrite.All | Optional — MFA methods and passkeys view, reset MFA, revoke passkey |
Device.ReadWrite.All | Optional — 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).
| Key | Action | Scope |
|---|---|---|
| / | Real-time filter | Both |
| S | Sort the list by any visible column (default: display name) | Both |
| i | Show inactive users (no sign-in for 30/60/90/180/365+ days, cycles on each press) | Both |
| d | User details (groups, licenses, MFA methods, passkeys, devices, password last set, last sign-in, created) | Both |
| e | Enable / disable account | Both |
| p | Reset password | Both |
| r | Refresh the list | Both |
| m | Modify profile attributes | Both |
| g | Group membership editor | Both |
| u | Unlock account | AD only |
| M | Move OU | AD only |
| k | Kill sessions (shows recent sign-ins first) | Entra ID only |
| R | Reset MFA | Entra ID only |
| P | Revoke a single passkey (FIDO2 security key or passkey in Microsoft Authenticator) | Entra ID only |
| v | Revoke a registered device | Entra ID only |
| l | Assign / remove licenses | Entra ID only |
| s | Switch context (no restart) | Both |
| x | Manage contexts (add/edit/delete/clone) | Both |
| L | Audit log viewer, filterable | Both |
| A | Re-authenticate (force a fresh browser login in place) | Entra ID only |
| n | Onboard user (guided account creation) | Both |
| o | Offboard user (checklist of retirement actions) | Both |
| Space | Select / deselect the user for a bulk action | Both |
| B | Bulk actions on the selected users | Both |
| Esc | Back: closes the current popup, picker or details view; from the user list, returns to the context manager | Both |
| q | Quit 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.