Skip to content

CLI

shunt 命令行 —— run、check、init、add、import、token、gateway login 和 provider login。

Updated View as Markdown

shunt import opencodex

离线读取兼容凭据并保存为新的私有快照,不覆盖现有设置、凭据文件或 OpenCodex 状态。请先预览;非交互式导出需要 --yes。

shunt import opencodex --dry-run
shunt import opencodex --provider cursor
shunt import opencodex --from /path/to/copied-opencodex --yes

导入 anthropic、openai-chat 和 openai-responses 传输的当前 API 密钥,以及 Cursor 或 Command Code 的当前订阅访问令牌(支持旧格式和多账户格式)。跳过过期、无效及不支持的记录。不复制刷新令牌或 Codex 账户池;其他订阅请单独登录。

--from 指定源目录(默认依次为 OPENCODEX_HOME、~/.opencodex)。可重复使用 --provider 选择多个提供商;省略时选择所有兼容的当前凭据。--output-dir 指定快照父目录(默认 ~/.shunt/imports)。每次创建唯一目录(0700)和 credentials.env(0600),保留以前的快照。导出仅支持 macOS/Linux。

启动 Shunt 前,在 shell 中 source 输出的文件路径。Cursor 使用 SHUNT_CURSOR_AUTH_TOKEN。API 密钥及 Command Code 访问令牌变量为 OPENAI_API_KEY、XAI_API_KEY、SHUNT_COMMANDCODE_API_KEY 或 SHUNT_IMPORTED_<PROVIDER>_API_KEY(大写,连字符变为下划线);请让现有配置的 api_key_env 与其匹配。不生成配置或路由。快照不会自动刷新,令牌过期后需重新导入。导入密钥不会启用 OpenCode Go 等尚未支持的提供商。

shunt run

启动网关。run 是默认子命令,因此裸的 shunt 也可以。

shunt run
shunt run --config /path/to/shunt.toml

启动时它会用绑定地址(默认 127.0.0.1:3001)记录 shunt listening。用 RUST_LOG 设置日志详细程度,例如 RUST_LOG=shunt=debug shunt run。

不带 --config 时,shunt 依次搜索 ./shunt.toml → ~/.config/shunt/shunt.toml → $HOMEBREW_PREFIX/etc/shunt.toml;带 --config 时,文件缺失即报错。见 配置。

通过 Homebrew 安装时,也可以作为后台服务运行: brew services start shunt。详见 安装。

shunt check

校验解析后的配置并退出(shunt --check 也可以):

shunt check
# -> config ok

报告具体错误:错误的 bind 地址、路由中的未知提供方、缺失的 api_key_env、错误的 base_url、错误的适配器/认证组合。

它还会执行与 shunt run 启动时相同的「已路由 Antigravity 凭据」检查:如果配置路由到原生 antigravity 上游却没有存储凭据,检查会直接失败并提示 shunt login antigravity,而不是先输出 config ok、再在启动时失败。该守卫是离线的 —— 只读取路由并探测凭据是否存在(单例文件或具名池候选项均可),既不刷新令牌也不访问后端。仅声明内置 antigravity 提供方但没有路由到它的配置不受影响,kind = "antigravity_cli" 路由同样不受影响。

配置了 [store] 时,检查不再完全离线:在 config ok 之后,它会以只读方式打开存储(读取 SQLite 文件,或连接 PostgreSQL),再输出一行。它不会执行迁移、修改数据库,也不会创建不存在的 SQLite 文件;但检查 WAL 模式的数据库时,SQLite 可能会创建或删除 -wal / -shm 辅助文件。

shunt check
# -> config ok
# -> store ok: sqlite sqlite:/var/lib/shunt/shunt.db, schema v1 (binary supports v1)
存储状态 config ok 之后的输出 退出码
已是最新 store ok: <backend> <target>, schema vN (binary supports vM) 0
schema 比二进制旧 同一行末尾追加 ; 1 pending migration applies at boot(K > 1 时为 ; K pending migrations apply at boot) 0
SQLite 文件不存在 store ok: sqlite sqlite:<path>, file does not exist yet; it is created with mode 0600 at boot 0
无法连接 store check failed,原因为 store <target> is unreachable: <reason> 1
schema 比二进制新 store check failed,原因为 store <target> is at schema version N, newer than the M this binary supports; upgrade shunt 1

<target> 是 sqlite:<path> 或 postgres://host:port/db,不会输出任何凭据。检查最长 15 秒。校验不通过的 [store](例如没有 sslmode=verify-full 的远程 PostgreSQL 主机)会更早以 config check failed 失败。

shunt init

在现有目录中创建 starter shunt.toml。该命令只写入这一个文件,不会安装任何内容或访问网络。

shunt init
shunt init --upstream codex --upstream kimi
shunt init --root /path/to/project
shunt init --force

不指定 upstream 时,starter 完全由注释组成,加载后使用 shunt 的默认 passthrough 配置。重复使用 --upstream 可按 failover 顺序添加 preset 条目;可用名称为 anthropic、codex、openai、xai、grok、kimi、cursor、kimi-code、zhipu、minimax-cn 和 opencode。Starter 不设置 server.default_provider,因此未映射流量仍使用默认 Anthropic passthrough。指定 preset 时,会在末尾自动追加一个 anthropic passthrough upstream(除非你自己指定了 anthropic),使生成的文件既能通过 shunt check,又能保留该 fallback。

Root 默认为当前目录,并且必须已经存在。如果其中已有 shunt.toml、shunt.yaml 或 shunt.yml,则不带 --force 时会停止且不写入任何内容。强制 init 只覆盖 shunt.toml;YAML variant 保持不变,config discovery 会优先使用新的 TOML 文件。

shunt add

获取面向编码 agent 的内置 Markdown blueprint。Blueprint 是实现指南,而不是安装程序:该命令不会修改文件、安装任何内容或访问网络。

shunt add                                      # 列出两种 blueprint kind
shunt add upstream                             # 列出具名 upstream 指南
shunt add upstream kimi --print                # 打印一份指南
shunt add upstream https://example.com/docs    # 调研兼容 endpoint
shunt add provider https://example.com/docs    # 调研源码集成

Kind 分为 upstream(配置已提供的 preset 或兼容 endpoint)和 provider(贡献新的 provider protocol 支持)。已知的 upstream slug 或 alias 会获取对应的具名指南。绝对 http:// 或 https:// URL 会注入该 kind 的通用 research 指南;相对路径会被拒绝。

Blueprint Markdown 始终输出到 stdout,以便直接输送给 agent。--print 明确表达这一意图并抑制交互式 stderr hint,但不会改变 stdout 内容。

shunt add upstream kimi --print | claude

shunt token

将一个 Claude 订阅 OAuth token 打印到 stdout(日志走 stderr),设计用来接入 Claude Code 的 apiKeyHelper。两种模式:

  • 静态 —— 如果设置了 SHUNT_GATEWAY_TOKEN 或 CLAUDE_CODE_OAUTH_TOKEN,原样回显该值。把它指向一个 claude setup-token 值,则从不刷新任何东西。
  • 自动刷新 —— 否则读取 ~/.claude/.credentials.json(用 CLAUDE_CREDENTIALS 覆盖路径),返回 claudeAiOauth 访问 token,并在它距 expiresAt 5 分钟以内时,针对 platform.claude.com/v1/oauth/token(与 Claude Code 使用的同一授权)刷新它,然后以 0600 原子写回新 token,保留其他所有字段。刷新只在实际过期时发生,以尊重该端点的速率限制。
// ~/.claude/settings.json
{
  "apiKeyHelper": "/path/to/shunt token"
}

何时需要它,见 连接 Claude Code。

shunt gateway login

让这台机器登录到启用了 [server.gateway] 的 shunt 部署,使用与 Claude Code 自身 /login 相同的 OAuth device flow。这是网关的客户端一侧;shunt login <provider> 和 shunt token 保持不变,仍然用于让 shunt 自己向上游认证。

shunt gateway login https://gateway.example.com
shunt gateway login https://gateway.example.com --manual   # 只打印 URL,不打开浏览器

shunt 会读取该部署的 /.well-known/oauth-authorization-server 文档并使用其中公布的端点,打印一个验证 URL 和一个短用户码,打开浏览器,然后轮询直到你在网关的 /device 页面上批准。discovery 返回 404 通常意味着该部署没有 [server.gateway] 配置段。discovery 和 device code 请求各自都有上限,因此只接受连接却从不应答的部署会以超时形式报告,而不会让终端无限期等待;随后的批准轮询则刻意不作同样限制,因为等待正是它的职责。非回环的明文 http:// URL 也会被接受,但会给出警告 —— device code 和 refresh token 会以未加密的形式传输,而且不只是这一次登录:之后的每一次令牌刷新都走同一条链路,并且 shunt gateway token 每次真正执行刷新时都会重复该警告(直接返回缓存 token、不发起网络调用时则不会)。而这样的登录会一直走完:明文 http 网关可以公布明文端点,但只能是它自己的 origin —— scheme、主机和端口都必须与你输入的 URL 一致。位于其他 origin 的明文端点会被拒绝 —— 只有一个例外,而且它对所有网关都成立,包括通过 https 访问的网关:指向回环地址的端点从不离开本机,因此始终被接受。网关签发的访问令牌会在任何会话被写入之前,先经过下文 shunt gateway token 所用的同一道 apiKeyHelper 校验:无法签发合规令牌的部署会直接在登录阶段失败,而不是留下一个报告成功、却永远无法使用的会话。

对该部署返回的内容还有两条底线。shunt 只会把 device code 和 refresh token POST 到 discovery 文档以 https 公布、以 http 指向回环地址、或者 —— 当你提供的网关 URL 本身就是明文 http 时 —— 以 http 指向该网关自身 origin 的端点;其他端点不会收到凭据,而是让 discovery 指名失败。最后这条放宽是依据你输入的 URL 计算的,而不是依据文档,因此一份被伪造或被中间人篡改的 discovery 文档无法指定第三方明文主机、把你的 refresh token POST 过去。验证 URL 也只在其为 http 或 https 时才自动打开 —— 该字符串由网关而非 shunt 选择,因此其他协议只会打印出来并说明未打开的原因,由你自己决定是否访问。

会话保存在 ~/.shunt/gateway/session.json(可用 SHUNT_GATEWAY_SESSION_FILE 覆盖),以仅属主可读写的方式写入:0700 目录中的 0600 文件。

shunt gateway token

只把网关访问 token 打印到 stdout —— 所有消息、警告和提示都走 stderr —— 因此可以直接接入 Claude Code 的 apiKeyHelper:

// ~/.claude/settings.json
{
  "apiKeyHelper": "/path/to/shunt gateway token"
}

它与打印上游 Claude 订阅 token 的 shunt token 是两个不同的 helper。两者不会读取对方的存储,而名字相近的 SHUNT_GATEWAY_TOKEN 变量属于 shunt token,不属于本命令。

已保存的 token 会一直使用到距离过期不足 5 分钟,然后才刷新。网关的 refresh token 会轮换且只能使用一次,因此刷新在独占文件锁下执行,并在拿到锁之后重新读取会话:多个 Claude Code 会话同时调用 helper 也只会发生一次刷新,不会因重放而使整个轮换家族失效。shunt gateway login 和 shunt gateway logout 也会获取同一把锁,并在各自写入完成前一直持有,因此进行中的刷新不会因为写回晚到而覆盖掉它们的结果。

锁和刷新都不会无限期等待。与网关的每次往返有上限,等待锁也有上限 —— 否则一个只接受连接却从不应答的部署会一直握着锁,让该机器上其他所有 apiKeyHelper 毫无提示地卡住。超出上限时命令会失败,并指出锁文件的路径。

进入 5 分钟缓冲期意味着该刷新了,而不是必须刷新。如果刷新失败而缓存的 token 仍然真正有效,shunt 会在 stderr 上给出警告并继续提供该 token,而不是把一次短暂的网络抖动变成认证失败。等它真正过期之后,命令才会失败。这条回退需要有证据表明什么都没有被轮换,因此它只覆盖两种可以确定从未出示过 refresh token 的失败:discovery 失败,以及连连接都没能建立的 token 请求。从请求发出的那一刻起,网关就可能已经轮换了 token,所以超时、读不出或解析不了的响应,以及网关指明的任何错误,都会让命令失败:在那种情况下继续提供缓存 token,会把一个已经用掉的 refresh token 留在磁盘上,等着下一次运行重放。凡是 Claude Code 的 helper 校验器会拒绝的 token —— 不是 1~16384 个不含空白的可打印 ASCII 字符 —— 都会在这里被拒绝并给出指明网关的诊断,而不是被打印出去、导致认证失败却没有任何说明。shunt gateway logout 会删除会话,但会刻意保留那个空的同名 .lock 文件:logout、login 和刷新正是靠这个 inode 相互串行化的,删掉它会让进行中的持有者继续锁着旧 inode,而下一个进程锁的是新建的 inode,两者就不再互斥了。

shunt gateway claude

无需编辑任何设置文件即可让 Claude Code 连接网关运行:

shunt gateway claude
shunt gateway claude -p 'summarize this repo' --model opus

它在运行 claude 时附带一个内联 --settings 文档,把 ANTHROPIC_BASE_URL 指向已保存会话中的网关,并把 apiKeyHelper 设为本二进制的绝对路径加 gateway token。该文档只作用于这一个进程:~/.claude/settings.json 完全不会被修改,并且它的优先级高于 shell 中已经 export 的 ANTHROPIC_BASE_URL(针对 Claude Code 2.1.234 实测)。

被启动的进程还会从环境中移除 34 个凭据与 provider 模式相关的变量:Anthropic、AWS 和 Foundry 的凭据变量,CLAUDE_CODE_USE_* provider 选择器及其区域、项目等同族变量,CLAUDE_CODE_SKIP_*_AUTH 修饰符,宿主托管的间接变量,ANTHROPIC_CUSTOM_HEADERS,以及两个 *_FILE_DESCRIPTOR 读取器 —— 此外还会在启动时解析并移除 CLAUDE_CODE_HOST_AUTH_ENV_VAR 所指向的那个变量,因为它保存的是任意名称,任何固定列表都无法表达。正是这一步让 apiKeyHelper 真正可达:settings 的 env 块是叠加在继承来的环境之上的,否则一个已 export 的 ANTHROPIC_AUTH_TOKEN 会直接压过 helper(helper 只在该变量缺席时才会被调用),而 CLAUDE_CODE_USE_GATEWAY 会把会话切换到网关 provider 模式 —— 那条路径根本不会调用 helper。ANTHROPIC_BASE_URL 被刻意保留 —— 它由 settings 文档注入。

这样关闭的只是环境变量这一条通道。凭据仍然可能通过 settings 文件的 env 块(Claude Code 在启动后会重新应用)、你自己设置里已有的 apiKeyHelper、凭据存储中已保存的登录,或者两个 file-descriptor 读取器在没有任何变量时也会查阅的 well-known 路径回退,抵达被启动的客户端。仅凭这一点并不能保证会话运行在 first-party 模式下。

claude 之后的参数都会原样转发。只有当它们出现在参数列表最前面时才有两个例外:--help 会打印 shunt 的帮助,而 --config 会被明确报错拒绝,而不是被 shunt 的全局选项悄悄吞掉。要把它们交给 claude,请放在 -- 之后:

shunt gateway claude -- --config /path/to/claude/config

由于凭据是通过 apiKeyHelper 提供的,会话不会变成已登录的网关会话,而是保持 Claude Code 通常的 first-party 模式 —— token 落在哪个凭据槽位决定了模式。关于这样做会放弃什么,参见网关登录。

shunt gateway logout

删除已保存的会话。幂等 —— 删除一个已经不存在的会话同样成功。

shunt gateway logout

shunt login claude

用以下三种模式之一创建一个由 shunt 管理的 Anthropic 池账户:

# Full OAuth: shunt 获取并存储一个新的可刷新登录(推荐)。
shunt login claude --name primary --mode oauth

# 导入当前可刷新的 Claude Code 登录。
shunt login claude --name imported --mode import

# 运行 Claude 的一年期、仅推理 setup-token 流程。
shunt login claude --name ci --mode setup-token

在 TTY 中省略 --mode 时,shunt 会提示选择 oauth、import 或 setup-token,并默认推荐 OAuth。非交互输入继续沿用原有的 import 默认值。--long-lived 保留为 --mode setup-token 的 deprecated alias。

--mode oauth 运行 shunt 的 full-scope PKCE 授权流程,并同时存储 access token 与 refresh token。默认情况下,shunt 在 127.0.0.1 上绑定一个临时 listener,打开授权 URL,并在浏览器返回 http://127.0.0.1:<port>/callback 时完成。如果无法打开浏览器、无法启动 listener,或 5 分钟内没有收到 callback,它会回退到隐藏输入的手动粘贴流程。通过 SSH 或在 headless 环境中可用 --manual 立即使用手动流程:

shunt login claude --name remote --mode oauth --manual

--mode import 将 ~/.claude/.credentials.json(或 CLAUDE_CREDENTIALS)复制到 ~/.shunt/accounts/claude/<name>.json。它保留 refresh token,关联 Claude Code 全局配置中的当前账户 UUID,而 shunt 刷新这份私有副本,不会修改 Claude Code 的源文件。

--mode setup-token 运行与 claude setup-token 相同的一年期、仅推理 PKCE 流程。在浏览器批准后,把显示的授权码粘贴到 shunt 的隐藏输入提示中。shunt 直接交换该代码,存储 opaque token 和签发账户 UUID,并且绝不打印 token。

在 Unix 上,文件以 0600 权限原子写入 0700 目录。SHUNT_CLAUDE_ACCOUNTS_DIR 可覆盖存储目录;复用名称会替换其文件。已有的外部 setup token 仍需要 token_env 加显式 uuid,因为签发后无法恢复其账户 UUID。

通过只带名称的池条目引用结果,或将 provider 的账户列表留空以扫描所有存储文件:

[[providers.anthropic.accounts]]
name = "primary"

shunt login xai

运行 xAI 的 device-code OAuth 流程并保存可刷新的 credential:

shunt login xai
shunt login xai --name <account-name>

不带 --name 时,credential 保存到 ~/.shunt/xai-auth.json(可用 SHUNT_XAI_AUTH_FILE 覆盖)。带 --name 时,它改为保存到 ~/.shunt/accounts/xai/<account-name>.json(0600;shunt 新建的存储目录权限为 0700,但已存在的目录保持其原有权限;SHUNT_XAI_ACCOUNTS_DIR 可覆盖存储目录),因此命名登录绝不会覆盖单账户文件。复用名称会替换其文件。--mode 和 --long-lived 会被拒绝,因为 xAI OAuth token 始终可刷新;--manual 仅限 claude 使用。

在提供方上用 [[providers.grok.accounts]] 条目或 auth = { mode = "xai_oauth", accounts = [...] } 引用命名账户;若都不配置,则扫描整个存储。若没有配置账户且存储为空,提供方会回退到单账户文件;一旦存在任何命名账户,就不再读取该文件。两个文件使用相同格式,因此在停止 shunt 后,先以仅属主可访问的方式创建存储目录,再移动文件,即可把现有登录移入账户池:install -d -m 700 ~/.shunt/accounts/xai && mv ~/.shunt/xai-auth.json ~/.shunt/accounts/xai/<account-name>.json。从单账户配置迁移时,该目录尚不存在。

xAI 每次刷新都会轮换刷新 token,因此每个可刷新登录只能有一个所有者:绝不要把同一个 credential 文件复制到两个位置,也不要在两个正在运行的 shunt 进程之间共享。

shunt login kimi

运行 Kimi Code 的 device-authorization(RFC 8628)OAuth 流程,并存储一个可刷新的 credential:

shunt login kimi --name <account-name>

--name 是必需的;省略它会失败,报错 `shunt login kimi` requires --name <account-name>。由于 Kimi Code OAuth token 始终可刷新,--mode 和 --long-lived 会被拒绝;--manual 仅限 claude 使用。

登录时会生成该账户的 X-Msh-Device-Id 一次,之后每次刷新都会复用,因此该账户向 Kimi 呈现的是稳定的设备身份。目前尚未观测到任何 Kimi 登录响应携带稳定的 upstream 账户标识符,所以扫描到的条目的池身份就是它自己的文件名。

在 Unix 上,文件以 0600 权限原子写入。shunt 创建的存储目录权限为 0700。SHUNT_KIMI_ACCOUNTS_DIR 可覆盖存储目录;复用名称会替换其文件。

其结果由使用 auth = "kimi_oauth" 的 provider 消费,该设置仅在 kind = "anthropic" 且 base URL 为 kimi.com 时才有效。通过只带名称的池条目引用:

[[providers.kimi-code.accounts]]
name = "primary"

配置只能用 [[upstreams]] 或 [providers.*] 之一声明 provider,不能同时使用两者。在 [[upstreams]] 配置中,改为在 auth 映射里指定账户: auth = { mode = "kimi_oauth", accounts = ["primary"] }。

Kimi 在每次刷新时都会轮换 refresh token,而且签发的 access token 生命周期只有约 15 分钟,因此刷新非常频繁。上面“每个可刷新登录只能有一个所有者”的规则在这里完全适用:多个 shunt 进程共享同一个 Kimi 账户文件时,第一次刷新就会让彼此失效。

完整的 upstream 配置见 Kimi。

Anthropic 账户池认证

对于 auth = "claude_oauth" 的 Anthropic provider,账户可以使用只带名称的存储条目、credentials = "~/.claude/.credentials.json" 或 token_env = "YOUR_ENV_NAME"。存储条目可由上面的 Full OAuth、Claude Code 登录导入或 setup-token 流程创建。完整配置和 failover 规则见 Anthropic 多账户。

环境变量

变量 效果
SHUNT_*(如 SHUNT_SERVER__BIND) 覆盖任意配置键;__ 分隔嵌套键
RUST_LOG 日志过滤器,如 shunt=debug
SHUNT_CLIENT_TOKENS 面向 [server.auth] 的客户端 token(名称可通过 tokens_env 配置)
SHUNT_GATEWAY_TOKEN / CLAUDE_CODE_OAUTH_TOKEN 面向 shunt token 的静态 token
SHUNT_GATEWAY_SESSION_FILE shunt gateway 系列命令使用的会话文件(默认 ~/.shunt/gateway/session.json);与上面的 SHUNT_GATEWAY_TOKEN 无关
CLAUDE_CREDENTIALS 面向 shunt token 和可刷新 shunt login claude 导入的备用 credential 文件路径
SHUNT_CLAUDE_ACCOUNTS_DIR shunt 管理的 Claude 账户存储的备用目录
SHUNT_KIMI_ACCOUNTS_DIR shunt 管理的 Kimi Code 账户存储的备用目录
SHUNT_XAI_AUTH_FILE 不带 --name 的 shunt login xai 所用的备用 credential 文件(默认 ~/.shunt/xai-auth.json)
SHUNT_XAI_ACCOUNTS_DIR shunt 管理的 xAI 账户存储的备用目录
由 token_env 指定的按账户变量 Anthropic claude_oauth 池条目的 setup token;原样使用
OPENAI_API_KEY openai 提供方的默认密钥环境变量(每个提供方通过 api_key_env)
Navigation

Type to search…

↑↓ navigate↵ selectEsc close