configuration reference - WireCatLabs/tg-cli GitHub Wiki
Use this page when you need the exact name, type, default or scope of a setting, or the name of an
environment variable. It lists every setting tg reads, every way to override it, and which value
wins. For everyday changes and an example file, start with the configuration guide.
How commands, output and exit codes behave is in the CLI contract.
Words this page uses:
-
Section: a part of the settings file.
defaultsapplies to every profile,profiles.<name>to one profile,personal.*only to personal-account commands, andbot.*only to bot commands. - Scope: the sections where a setting is allowed. A setting outside its scope is an error.
- Source: where the value in force came from: an option, an environment variable, a section of the file, or the built-in default.
No setting can hold a secret: the file has no field for a session, an app hash, a phone number or a chat id.
A short example file and a full one with every section, each part explained, are in the configuration guide.
For each setting, the first of these that is set:
- an option on the command line (
--limit 50,--record,--timeout 30s) - an environment variable (
TG_PROFILE,TG_TIMEOUT) - the profile's own entry in the config file
-
defaultsin the config file, shared by every profile - the built-in default
tg personal chats list --limit 5 # 5: the option
# "limit": 50 in the profile's entry β when there is no option
# "limit": 30 in "defaults" β when the profile has none either
# 20 β when nothing is setInside the file, the most specific section wins. For a personal-account command on the profile
work: personal.profiles.work, then profiles.work, then personal.defaults, then defaults.
For tg work bot β¦ the same, with bot in place of personal.
Not every setting has all five ways. The table of settings says which ones exist.
tg config show
tg work config show
tg shop config show --botIt lists the profile, where the profile name came from, the profiles the file names, the path of
the file and whether it exists, and every setting with its value and where that value came from:
flag, an environment variable, config file, config defaults or default. --bot shows the
settings as a bot command of that profile gets them. --json gives the same as one object, for a
script.
The list ends with commandTimeoutMs: the limit on a whole command, from --timeout or TG_TIMEOUT.
It is not a setting of the file; timeout_ms limits one request.
When TG_CONFIG_DIR, TG_STATE_DIR or TG_CACHE_DIR is set, it says so on stderr, since that also
changes which login is found (where the login parts are kept).
β It is not a health check. It creates the starter configuration if absent, then reads files. It
opens no store, asks no keyring and does not connect. Whether the session still works is a question
for tg personal doctor --online (troubleshooting with tg doctor).
config.json in the settings directory (~/.config/tg-cli/config.json on Linux; other systems are
listed in where files go). The first command that reads settings
creates it with limit, keep_runs_for_days, sends_per_hour, update_check and skill_hint under
defaults; an existing file is never replaced.
{
"default_profile": "default",
"defaults": {
"sends_per_hour": 10,
"update_check": false
},
"profiles": {
"default": { "limit": 50 },
"work": { "permissions": { "messages": "readonly", "messages.send": "allow" }, "record": true }
},
"personal": { "defaults": { "catch_up_marks_read": true } },
"bot": { "profiles": { "shop": { "sends_per_hour": 200 } } }
}-
defaults: every profile, personal accounts and bots. -
profiles.<name>: one profile, whichever way it is used. -
personal.defaults,personal.profiles.<name>: only personal-account commands. -
bot.defaults,bot.profiles.<name>: onlytg <name> bot β¦commands.
default_profile at the top names the profile used when neither the first word nor TG_PROFILE
names one. The first word (tg work β¦) and TG_PROFILE override it. Without any of them, a command
that uses an account stops with exit code 2 and lists the profiles on this computer.
| Setting | Default | What it does | Overridden for one run by |
|---|---|---|---|
limit |
20 |
rows per page of a list | --limit |
timeout_ms |
none | how long one request to Telegram may wait, in milliseconds. A command makes several, so for a limit on the whole command use --timeout
|
none (--timeout is a different thing) |
color |
from the terminal | colour in the table view | none; with no setting, NO_COLOR turns it off |
sender_colors |
false |
a colour per sender in the table view of messages | none |
catch_up_marks_read |
false |
inbox and review mark each chat they show read, up to the newest message shown. The other side sees it |
--mark-read, --no-mark-read
|
search_catch_up |
false |
messages sync and store gaps repair also prepare the fetched chat for local search: its graph and, where installed, its vectors. Never downloads models or calls remote providers |
--catch-up, --no-catch-up
|
picture_search |
false |
search stored pictures by what they show. images embed reads the stored pictures once the model is downloaded, and then search messages also returns a pictures list (pictures) |
none |
record |
false |
keep every run (diagnostics) |
--record, --no-record
|
keep_runs_for_days |
30 |
recorded runs older than this are removed when the next one is kept | none |
permissions |
everything allowed, except: deleting, ending sessions and a few other changes ask; reply rules may not send | what the profile may do, per command (below) | none; --yes and --allow-dangerous only answer ask, they never lift deny
|
sends_per_hour |
30; a bot has none until it is set in the bot section |
the most sends in any hour (the send guard) | none |
requests_per_minute |
60 |
requests a minute after a burst of 20, shared by every process of the profile; 0 turns it off (limits and waits) |
TG_REQUESTS_PER_MINUTE |
transcribe_with |
auto |
who turns voice into text: auto (Telegram, else a local model), messenger or local
|
--local, or --model, which implies it |
speech_model |
none | which downloaded model --local uses (tg personal models audio list) |
--model |
proxy |
none | the SOCKS5, HTTP CONNECT or MTProxy server to reach Telegram through (below) |
TG_PROXY |
read_other_bots |
false |
a bot only: whether tg personal bot may read what other bots on this machine kept β true, or a list of profile names (bots) |
none; --all-bots and --bots ask, the setting allows |
update_check |
true |
the daily "a newer version exists" line; only under defaults
|
none; TG_NO_UPDATE_CHECK, NO_UPDATE_NOTIFIER or CI turn it off |
skill_hint |
true |
a line, at most once a day, for an agent whose copy of tg's skill is missing or older than tg; only under defaults
|
none |
embedding_provider |
local |
local model or openai
|
--provider |
embedding_model |
provider default | embedding model | --model |
embedding_base_url |
provider default | embedding API endpoint | --base-url |
embedding_dims |
model default | integer 1β65,536 | --dims |
analysis_provider |
agent |
agent, openai or anthropic
|
build --provider |
analysis_model |
none; required for --analyze
|
analysis model | build --model |
analysis_base_url |
provider default | analysis API endpoint | build --base-url |
models |
none | external models by purpose (below) |
TG_MODELS_* variables |
searchStemmers.cyrillic, searchStemmers.latin
|
russian, english,spanish
|
the word-stem languages for search in the whole store (below) | none |
The file is written 0600 when first created and 0644 by config set. It holds no secret.
permissions is an object: each key is a command path, each value a level.
{ "profiles": { "work": { "permissions": { "messages": "readonly", "messages.send": "allow" } } } }| Level | What happens |
|---|---|
deny |
nothing, not even reading: refused with exit code 5 before connecting |
readonly |
reading works; a change is refused with exit code 5
|
ask |
a y/N question in the terminal, no by default (below) |
allow |
it goes ahead and never asks |
A key is a command path: messages, messages.delete, messages.send, reactions,
polls.vote, chats.mark-read, chats.members.remove, contacts, account.sessions.end. It
starts with a resource β messages, reactions, polls, topics, chats, contacts, account,
bot, conversations, tags, search, searches, tasks, replies, attachments, stats,
store or metadata β and must name a known command or checked write. Unknown command keys are
refused by config set with exit code 2, including keys inside a whole permissions object.
config unset can remove an old unknown key. Reading an existing file with one warns on stderr and
continues.
The most specific key you set wins: with the example above, messages.send is allowed and every
other change to messages is refused. There is no wildcard: messages: readonly does not touch
reactions, polls or chats.
Keys from different sections of the file add up, but the nearest section decides first, then the
longest key. A key a profile sets hides the same key and every key under it in personal.defaults,
bot.defaults and defaults. Here profile agent cannot delete: its messages hides
messages.delete from defaults.
{
"defaults": { "permissions": { "messages.delete": "allow" } },
"profiles": { "agent": { "permissions": { "messages": "readonly" } } }
}It works both ways: a profile's messages: allow also hides messages.delete: deny from defaults,
and deleting asks again, as it does by default.
inbox, review, watch, serve and messages sync, export and search show messages, so they
count as messages: messages: deny stops them too. config, session, doctor, recipients,
mcp and the store's own upkeep are never limited.
The defaults allow everything except a few changes that are hard to reverse. These ask: messages.delete,
bot.messages.delete, chats.delete, chats.clear, topics.enable, topics.delete and
account.sessions.end. Reply rules may not send until you allow it: replies.send is deny. A
built-in default only tightens: messages: readonly still refuses a deletion, and messages: allow
keeps the question before a deletion until you set messages.delete itself.
tg config set permissions.messages.delete allow # delete without the question
tg config set permissions.messages.send ask # ask before every send
tg config unset permissions.messages.delete # back to the defaultTo refuse every change in Telegram from a profile β here the profile agent β set each resource:
for key in messages reactions polls topics chats contacts account conversations tags searches replies attachments bot; do
tg agent config set permissions.$key readonly
doneChanges kept only on this computer β tasks, store and metadata β have their own keys.
At level ask, tg shows what will change and asks go ahead? [y/N]. An answer other than y
does nothing and ends with exit code 130. A flag answers yes for you: --allow-dangerous for a
deletion that cannot be undone (messages, a chat, its history or a topic), the global --yes for
any other change. With no terminal, or under --json or --jsonl, nobody can answer: the change is
refused with exit code 7, confirmation_required, and the error names the flag.
MCP has no server confirmation forms. deny and readonly block writes; ask and allow permit
the requested write. Repeat --permission key=level for temporary server permissions
(browser setup). CLI confirmation at ask still applies.
replies.send defaults to deny; enabling a reply rule alone does not allow sending.
tg personal replies audience can limit replies to selected people or exclude some.
permissions is the only access setting. A file that still holds read_only or allow is refused as an
unknown setting: replace them with permissions keys (read_only: true is every resource readonly).
tg config set limit 50 # this profile
tg work config set permissions.contacts readonly # profile "work"; one key at a time
tg config set sends_per_hour 10 --defaults # every profile
tg config set limit 30 --personal --defaults # every personal account
tg shop config set sends_per_hour 200 --bot # only the bot "shop"
tg config set update_check false --defaults # a setting that exists only under defaults
tg config unset sends_per_hour # back to the defaultconfig set checks the value against the same rules the reader uses, so it never writes a file
that a later command refuses.
searchStemmers.cyrillic (russian or none) and searchStemmers.latin (english, spanish,
both comma-separated β the default β or none) are kept in the shared local store, not in the
settings file: one value for every profile and for both CLIs. So --defaults, --personal and
--bot are not accepted with them, and a process under TG_PROFILE_LOCK cannot change them. config unset restores the built-in value. After a change,
rebuild the search index (index maintenance).
A setting the file does not know stops every command, with exit code 3:
config.json is not a valid config:
profiles.default.limt: unknown setting β the known ones are limit, timeout_ms, β¦
A misspelled setting that was silently ignored would run with the default and never say why. The
same holds for a key in permissions that does not start with a resource.
Where Telegram is blocked, tg can reach it through a proxy: SOCKS5, an HTTP proxy that allows
CONNECT, or an MTProxy. One proxy setting per profile, or for every profile with --defaults.
Set it before tg personal setup: the login goes through it too.
tg config set proxy socks5://proxy.example:1080 # no password: on the command line
tg config set proxy http://[email protected]:3128 # a user without a password
tg config set proxy - # with a password or an MTProxy secret
proxy URL, hidden as you type: tg://proxy?server=mt.example&port=443&secret=eeβ¦
tg config unset proxy| Form | Kind |
|---|---|
socks5://[user:password@]host[:port] |
SOCKS5; port 1080 when none is given; socks5h:// is read the same way |
http://[user:password@]host[:port] |
an HTTP proxy, by CONNECT; https:// reaches the proxy itself over TLS |
tg://proxy?server=β¦&port=β¦&secret=β¦ or https://t.me/proxy?β¦
|
an MTProxy, as Telegram shares it; FakeTLS (eeβ¦) secrets work |
tg://socks?server=β¦&port=β¦&user=β¦&pass=β¦ |
Telegram's share link for a SOCKS5 proxy |
A password or an MTProxy secret never reaches the settings file. config set proxy - reads the
URL without echo, or from a pipe, keeps the secret in the OS keyring β one per profile, and one for
--defaults that a profile uses only with the defaults' proxy β and writes the URL without it;
config show, doctor and the errors print it the same way. A URL with a secret on the command line
is refused, because ps and your shell history would keep it.
TG_PROXY takes the whole URL, secret included, and wins over the setting β for CI, or for one try.
config show prints the setting from the file; tg personal doctor prints the proxy actually in use.
ALL_PROXY and HTTPS_PROXY are not read: they are usually set for other tools, and a proxy is
something you choose for this account.
The Bot API (tg personal bot β¦) and the app registration of session start --app auto go through the
same SOCKS5 or HTTP proxy. An MTProxy carries only Telegram's own protocol, so with one they connect
directly; tg personal doctor says which. --app browser opens my.telegram.org in your browser, which
uses its own proxy settings.
| Variable | What it does |
|---|---|
TG_PROFILE |
the profile, when the first word does not name one |
TG_PROFILE_LOCK |
pins the process to one profile; any other is refused (profiles) |
TG_TIMEOUT |
the same as --timeout: 500ms, 30s or 2m for the whole command |
TG_REQUESTS_PER_MINUTE |
the same as requests_per_minute, and wins over it |
TG_API_ID, TG_API_HASH
|
the app, instead of the keyring β for CI; both or neither |
TG_PROXY |
the proxy URL, password or secret included; wins over the proxy setting (above) |
TG_CONFIG_DIR, TG_STATE_DIR, TG_CACHE_DIR
|
move the three directories β and the keyring entry with them |
MESSAGING_STORE |
the path of the local store file |
WIRECAT_STATE_DIR |
the WireCat folder every CLI shares: the local store and the speech models |
TG_NO_UPDATE_CHECK |
1 turns off the daily "a newer version exists" line |
NO_COLOR |
no colour in the table view |
XDG_RUNTIME_DIR |
on Linux, how the keyring is reached; cron and ssh often leave it out |
TG_EMBEDDING_*, TG_ANALYSIS_*, TG_MODELS_*
|
model settings (below) |
Point the three directories somewhere else, and tg has a fresh config, login and runs there. It
does not see your usual login, because the keyring entry moves with them:
export TG_CONFIG_DIR=/tmp/tg-try/config TG_STATE_DIR=/tmp/tg-try/state TG_CACHE_DIR=/tmp/tg-try/cache
export MESSAGING_STORE=/tmp/tg-try/wirecat.db
tg personal setupWithout MESSAGING_STORE, what that login reads still goes into your usual local store.
βProfileβ includes root defaults, profiles.<name>, personal.defaults,
personal.profiles.<name>, bot.defaults and bot.profiles.<name> unless restricted below.
Unknown keys and invalid types are errors. Defaults and effects are listed above.
| Key | Accepted type/value | Scope |
|---|---|---|
default_profile |
profile-name string | file root |
limit, timeout_ms, keep_runs_for_days, sends_per_hour
|
integer β₯ 1 | profile |
requests_per_minute |
integer β₯ 0 | profile |
color, sender_colors, catch_up_marks_read, search_catch_up, record
|
boolean | profile |
permissions |
object of command paths and deny, readonly, ask, allow levels |
profile |
read_other_bots |
boolean or array of profile names | bot only |
update_check, skill_hint
|
boolean | root defaults only |
transcribe_with |
auto, messenger, local
|
profile |
speech_model |
downloaded model id as a string | profile |
proxy |
supported proxy URL as a string | profile |
embedding_provider |
local, openai
|
profile |
embedding_model, analysis_model
|
nonempty string, at most 200 characters | profile |
embedding_base_url, analysis_base_url
|
HTTP(S) URL without credentials, query or fragment | profile |
embedding_dims |
integer 1β65,536 | profile |
analysis_provider |
agent, openai, anthropic
|
profile |
models |
purpose objects described below | profile |
searchStemmers.cyrillic |
russian, none
|
shared store, through config set
|
searchStemmers.latin |
english, spanish, both comma-separated, none
|
shared store, through config set
|
models.<purpose> accepts only provider, model and base_url. Purpose names start
with a lowercase letter and contain lowercase letters, digits and hyphens.
default supplies common fields; analysis and replies override them field by field.
Other valid purpose names can be saved in advance; this does not enable an absent use case.
An absent provider or off disables external model calls.
| Field | Accepted value | Default |
|---|---|---|
models.<purpose>.provider |
off, openai, anthropic
|
absent; no external call |
models.<purpose>.model |
nonempty string, at most 200 characters | absent; an enabled provider needs an explicit model id |
models.<purpose>.base_url |
HTTP(S) URL without credentials, query or fragment | provider endpoint |
For each field, precedence is TG_MODELS_<PURPOSE>_PROVIDER, _MODEL or _BASE_URL,
then the nearest configured purpose field, explicit legacy analysis* fields for analysis,
then environment and configuration fields under models.default. Hyphens in a purpose
become underscores in its environment name. Credentials are not part of this object.
Embedding and analysis settings are independent and may differ by profile. Endpoints must be
HTTP/S without embedded credentials, query or fragment. Remote embeddings also receive the query
text of MCP searches. Keys are set with models text key set openai|anthropic and stay outside
config.json. Ordinary build does not start remote analysis: explicit --analyze is required.
The legacy fields accept TG_EMBEDDING_PROVIDER, TG_EMBEDDING_MODEL,
TG_EMBEDDING_BASE_URL, TG_EMBEDDING_DIMS, TG_ANALYSIS_PROVIDER,
TG_ANALYSIS_MODEL and TG_ANALYSIS_BASE_URL before file values; command options win over them.
TG_MODELS_DEFAULT_PROVIDER, TG_MODELS_DEFAULT_MODEL and TG_MODELS_DEFAULT_BASE_URL
supply common fields in the new format; replace DEFAULT with a purpose, such as ANALYSIS.
An empty variable does not override a setting. Invalid values return configuration_error.
-
Security: what
permissions, the recipient list andsends_per_hourprotect -
Diagnostics: what
recordkeeps and how longkeep_runs_for_daysholds it