How to Use ClickHouse CREATE TOKEN for Agent Credentials
Hand an AI agent your ClickHouse password and it keeps it: in logs, in its context window, in whatever it pastes into a ticket. ClickHouse CREATE TOKEN, added in ClickHouse 26.9 on September 23, 2026, mints a random secret that expires on its own and carries only a subset of your privileges.
You'll end with a minting user, a read-only token, and a launcher script for an agent, MCP server or CI job. Confirm you're on 26.9 or later with SELECT version().
1. Give a dedicated user the CREATE TOKEN privilege
A token is an extra password for the user who runs the statement, so it can never exceed that user's rights. Mint from a dedicated user, not a human's account. As an admin:
CREATE USER agent_minter IDENTIFIED WITH sha256_password BY '<main-password>';
GRANT SELECT, INSERT ON analytics.* TO agent_minter;
GRANT CREATE TOKEN ON *.* TO agent_minter;
CREATE TOKEN is a separate privilege because tokens weaken an account's security. The docs say ALTER USER on yourself also qualifies, but it is much broader.
2. Mint a read-only token with ClickHouse CREATE TOKEN
clickhouse-client --host <your-clickhouse-host> --secure \
--user agent_minter --password "$CH_MINTER_PASSWORD" \
--query "CREATE TOKEN VALID FOR INTERVAL 2 HOUR GRANTS (SELECT ON analytics.*) FORMAT TSVRaw" \
| cut -f1
ClickHouse stores the 32-character secret hashed and shows it once. The statement returns token and valid_until; FORMAT TSVRaw prints them unescaped and cut -f1 keeps the token.
VALID FOR INTERVAL 2 HOURadds two hours to now.VALID UNTIL '<datetime>'takes an absolute time. With neither, the token lasts 30 minutes (create_token_default_ttl_seconds, default 1800).GRANTS (...)intersects with the user's rights. It can subtract, never add.
3. Connect to ClickHouse with the token
The token is a password for the minting user, so you still send that user name. Prefer headers over ?password=, which the docs warn proxies may log.
# HTTP
curl --silent --show-error \
-H "X-ClickHouse-User: agent_minter" \
-H "X-ClickHouse-Key: $CH_TOKEN" \
--data-binary "SELECT count() FROM analytics.events" \
"https://<your-clickhouse-host>:8443/"
# Native client
clickhouse-client --host <your-clickhouse-host> --secure \
--user agent_minter --password "$CH_TOKEN" \
--query "SELECT count() FROM analytics.events"
4. Check the token can't write or mint more
clickhouse-client --host <your-clickhouse-host> --secure \
--user agent_minter --password "$CH_TOKEN" \
--query "INSERT INTO analytics.events SELECT * FROM analytics.events LIMIT 0"
Even though agent_minter holds INSERT, the token session should fail with ACCESS_DENIED (code 497), as in the release post's example. A session from a token with a GRANTS clause also can't create authentication methods, so a leaked token can't mint children.
5. Launch an agent or MCP server with a fresh token
Mint in your orchestrator, never inside the agent. env -u keeps the minter password out of the child process.
#!/usr/bin/env bash
set -euo pipefail
token="$(clickhouse-client --host "$CH_HOST" --secure \
--user agent_minter --password "${CH_MINTER_PASSWORD:?}" \
--query "CREATE TOKEN VALID FOR INTERVAL 4 HOUR GRANTS (SELECT ON analytics.*) FORMAT TSVRaw" \
| cut -f1)"
exec env -u CH_MINTER_PASSWORD \
CLICKHOUSE_HOST="$CH_HOST" CLICKHOUSE_USER=agent_minter \
CLICKHOUSE_PASSWORD="$token" CLICKHOUSE_SECURE=true \
uv run --with mcp-clickhouse --python 3.12 mcp-clickhouse
The variable names come from the open-source mcp-clickhouse server. It defaults to readonly=1, but its README says real enforcement comes from the ClickHouse user's grants, which is what the token narrows. For CI, swap the last line for your test command.
6. List and revoke ClickHouse tokens
There is no statement to drop a single token. Inspect them, then reset:
SELECT name, auth_type, auth_params, auth_grants
FROM system.users
WHERE name = 'agent_minter';
-- keep only the newest authentication method
ALTER USER agent_minter RESET AUTHENTICATION METHODS TO NEW;
-- or replace every method, tokens included, with one password you choose
ALTER USER agent_minter IDENTIFIED WITH sha256_password BY '<new-main-password>';
RESET AUTHENTICATION METHODS TO NEW keeps only the newest method, so if that is a token, you just removed your password. The second form is the safer revoke-everything.
Gotchas
- Expiry is checked at login only. Open sessions and running queries aren't interrupted, so pair a short
VALID FORwith short sessions. - Objects made in a token session outlive it. Refreshable materialized views, materialized views on Kafka, RabbitMQ, NATS or S3Queue, and dictionaries with a
LIFETIMEkeep running after expiry, with the creating user's full rights asDEFINER, not the token'sGRANTS. Never putCREATE TABLE,CREATE VIEW,CREATE DICTIONARYorACCESS MANAGEMENTin a token'sGRANTS. - Clusters need extra care.
ON CLUSTERisn't supported, andGRANTSis enforced only on the node that receives the query. Use replicated access storage, or runALTER USER ... ADD IDENTIFIED WITH sha256_hashon each node, and keep the minting user's own privileges tight. - Orphaned tokens count. If the result never reaches the client, the unusable method stays until it expires and counts against
max_authentication_methods_per_user. EachALTER USER,CREATE TOKENincluded, purges expired methods first. - Skip immortal tokens.
create_token_default_ttl_seconds = 0orVALID UNTIL 'infinity'turns a token back into a password.
A leaked agent credential is a question of when, so make sure the one in production is read-only and expires within hours.