Internet Identity (II) supports a CLI-auth flow that issues a delegation for a
session key held outside the browser. icp identity link web uses it to let
a terminal session — including an AI agent such as Claude Code — sign canister
calls as the user's app-specific principal: the same principal the user has
when signed in to that app's web UI (selected with --app <domain>).
The user still authenticates interactively in their own browser with their
passkey; the agent only ends up holding a time-limited delegation. The private
key of the user's II identity is never exposed.
-
First-time sign-in fails with a "CLI access disabled" screen. This is
the expected first-run path, not an error. Tell the user to enable CLI
access for their identity in II settings, then restart the flow from
scratch — the previous sign-in URL is single-use (its nonce and localhost
listener are dead). Plan for two rounds on first use.
-
The link command waits for an Enter keypress before doing anything. On
0.3.x it first prints Press Enter to log in at https://id.ai/cli and
blocks reading stdin; only after that does it start the flow and try to
open the browser. In a background or non-interactive run, pipe an actual
newline into it (printf '\n' | icp identity link web ...). Do NOT
redirect from /dev/null: a bare EOF does not satisfy the prompt on
0.3.2 — the command sits on Press Enter to log in and never starts the
flow, so you waste the first attempt and have to restart, prompting the
user twice. Feed the newline on the first try and the flow starts
immediately.
-
Don't assume the browser opened. Sandboxed or non-interactive shells
often can't launch a GUI browser. Read the command's output: if a full
sign-in URL is printed, show it to the user to open themselves. If the
output shows neither a browser launch nor a URL, report that — do not
construct a URL by guessing, and ask the user whether a browser window
appeared.
-
The command blocks until sign-in completes and gets killed by tool
timeouts. Run icp identity link web as a background task, then wait for
the user to confirm they finished signing in before checking the result.
The command must also be able to bind a localhost port; if a sandbox blocks
this, ask the user before rerunning unsandboxed.
-
Relying on the default identity signs with the wrong principal. Every
icp command acting as the user must pass --identity <NAME> explicitly.
Never depend on project or global default identity settings, and never
change them.
-
Reusing or overwriting identities. Never reuse an existing identity,
even if icp identity list already shows one linked for the same app — its
delegation may be expired and it belongs to whoever created it. Always
create a fresh identity per session: check icp identity list, then pick
a unique name prefixed with the agent's own identifier,
<agent>-<app>-<YYYYMMDD-HHMM> — e.g. claude-oisy-20260611-1530 for
Claude, cursor-oisy-... for Cursor; use agent- if no identifier is
known. Never
overwrite an existing identity. icp identity list is read-only context
for picking a free name — never delete, rename, reauth, or otherwise
modify any identity you did not create this session, even ones with the
agent's own prefix left over from a previous run. Operate only on $NAME.
Why per-session and not one reused identity per app: --app derives the
same app principal for a given II identity, so a fresh identity grants no
extra authority isolation — the point is provenance and blast radius.
A per-session identity keeps icp identity list auditable (which session
created which live delegation), lets cleanup delete exactly that one, and
avoids a shared identity where another session's reauth silently mutates
a delegation this session is mid-task with.
When scanning icp identity list to pick a name, if you see multiple stale
agent-prefixed identities for the same app from past sessions (e.g. several
claude-oisy-* entries), offer to clean them up before proceeding: present
the list, wait for the user to confirm each one, then delete them with
icp identity delete <NAME>. Never delete without explicit confirmation.
-
Wrong principal because --app was omitted. Without --app, the
provider uses its default derivation origin and the resulting principal
will NOT match the user's principal in the target app. Pass the app's bare
domain (no scheme, port, or path), e.g. --app oisy.com.
-
Expired delegations. Delegations are time-limited; the lifetime is set
by the identity provider during sign-in. icp identity link web has NO
flag to control it — do not invent one (e.g. --ttl; 0.3.x rejects it and
the whole command fails). When calls start failing with signature/expiry
errors, run icp identity reauth <NAME> — the user must sign in again as
the same identity.
-
Canister calls without explicit arguments auto-cancel. When call
arguments are omitted, icp canister call opens an interactive prompt to
build the arguments and confirm sending (Do you want to send this message? [y/N]), which immediately cancels in a non-interactive shell
(User cancelled.). Always pass arguments explicitly — including the
empty tuple '()' for zero-argument methods like whoami.