Claude Code / Codex / Github Copilot 使用量消耗体感

张开发
2026/5/19 22:59:29 15 分钟阅读
Claude Code / Codex / Github Copilot 使用量消耗体感
最近同时使用Claude Code / Codex / Github Copilot 做开发Github Copilot 使用了Claude Opus模型消耗3倍流量。 三个工具都是使用了20刀左右每月的基础订阅模式其中Github Copilot是公司企业模式订阅的没有网络环境要求。在使用过程中关于限额消耗速度我的体感是Codex最省Github Copilot使用Claude模型模式下消耗稍快Claude Code消耗也较快,实现较大功能时能用1个多小时就到了5个小时限额Sonnet4.6可能和我当前会话用了2天有关系新会话的时候消耗没那么块, 但是因为Github Copilot限额是按月的没有5小时和每周的限制可以一次性使用更多但是每个月月中差不多就用完了不过gpt4.1模型不限量还可以继续用。以上是我使用的体感供大家参考另外建议使用Claude Code和Github Copilot按需选择模型不同模型消耗可能区别很大。一个更直观的关于Claude的使用情况这个5小时周期内我要求Claude Code针对我的工程进行一次review看看是否有安全问题逻辑漏洞等等然后基于这个会话进行了一次回复使用Opus4.6模型两倍消耗速度总共消耗5小时内限额的29%。 以下是会话输出内容和我的工程文件列表截图。 其中一个消耗比较高的原因也在于我没还没有执行/init有生成CLAUDE.md文件▐▛███▜▌ Claude Code v2.1.92 ▝▜█████▛▘ Opus 4.6 with medium effort · Claude Pro ▘▘ ▝▝ /mnt/d/RagDocuments ❯ /usage ⎿ Status dialog dismissed ❯ review下devops_cli工程关于工程信息可以看代码也可以参考docs下的内容playgroud下的东西可以不用看不是核心内容是用来跑web端模拟的一个外挂。主要看看核心工程有没有漏洞和逻辑上的问题也可以看看有没有扩展性的问题这个框架作为基础框架后续可以通过扩展插件让这个框架实现例如我的后端开发框架初始化脚手架的程序也可以是扩展成一个离线安装的运维工具。做这个的出发点 I decided to develop this framework to address a real-world need in my work. As a service provider, we are responsible for maintaining multiple client environments. Accessing client environments typically requires connecting through a VPN and a jump server, often implemented as a Privileged Access Management (PAM) system. Managing access across so many servers within each environment is cumbersome and inefficient. In addition, there is usually a designated server within each VPC that can SSH into other servers. When we are tasked with updating middleware, deploying software, or implementing security measures, we either connect to each server individually or access a DevOps server and execute remote commands from there. This approach requires maintaining a large number of shell scripts and is very time-consuming. It is especially inefficient for routine tasks such as daily health checks, where we need to collect data from all servers and generate reports. This framework significantly improves efficiency and saves a considerable amount of time and effort. ● Explore(Explore devops_cli project structure) ⎿ Done (38 tool uses · 106.1k tokens · 1m 41s) (ctrlo to expand) ● Let me read the most security-critical files directly to give you concrete findings. Read 8 files (ctrlo to expand) ● Now I have a thorough picture of the codebase. Here is the full review. --- devops_cli Code Review Overall the architecture is solid and the decisions are well-reasoned. The vault security model, sidecar boundary, and SSH runtime strategy are all sound. The findings below are organized by severity. --- Security Findings [HIGH] Plugin main path not sandboxed — path traversal possible plugins/loader.js:102 const mainPath path.resolve(entry.pluginDir, entry.manifest.main); path.resolve with a user-controlled manifest.main value escapes the plugin directory. A plugin.json containing main: ../../../etc/passwd (or any ../../ path) resolves outside pluginDir. While the threat model is local plugins only, a plugin directory accidentally installed from an untrusted source would let it load arbitrary files. Fix: const mainPath path.resolve(entry.pluginDir, entry.manifest.main); if (!mainPath.startsWith(entry.pluginDir path.sep)) { return { ...entry, status: invalid, errors: [Plugin main path escapes plugin directory.] }; } --- [HIGH] setTemplate/resetTemplate on SSH runtime is exposed to plugins ssh/runtime.js:709–721 Any plugin loaded via createPlugin() that receives the app context can call context.sshRuntime.setTemplate({ executable: /attacker/fake-ssh, args: [...] }) and redirect every subsequent SSH operation through an arbitrary binary — including operations that forward decrypted vault credentials. This is the most serious escalation path in the extensibility model. The plugin itself may be benign code that exposes this through an unguarded API call. Fix: Remove setTemplate/resetTemplate from the public context object exposed to plugins. Keep them as internal-only functions or gate them behind an explicit user confirmation flow. --- [MEDIUM] Unquoted jumpServer.host in Windows plink proxy command ssh/runtime.js:527–529 args.push(jumpServer.host, -nc, %host:%port); return args.join( ); jumpServer.host is pushed directly without windowsQuote(). The result is passed as a shell-interpolated string. Hostnames with embedded spaces or special characters (edge case, but valid in some DNS configurations) would break the proxy command string. Consistent with how other args are built, windowsQuote should be applied. Similarly jumpServer.username on line 512 is unquoted. --- [MEDIUM] Temp key files survive process kill ssh/runtime.js:57–74, crypto.js:64–84 Both createTempIdentityFile and deriveOpenSshPublicKey write private key material to os.tmpdir() and rely on finally blocks for cleanup. If the process receives SIGKILL (not SIGTERM), the finally block never runs and the key file persists in the system temp directory. Fix: Register a cleanup set at startup and use process.on(exit, ...) process.on(SIGTERM, ...). Or write temp key files into a mkdtemp-created directory under a controlled path with an O_TMPFILE-style approach where available. --- [LOW] Custom fingerprint format is incompatible with OpenSSH crypto.js:312–314 const fingerprint createHash(sha256) .update(publicKeyPem, utf8) .digest(base64); return SHA256:${fingerprint}; This hashes the PEM string (ASCII-encoded), not the raw key bytes. OpenSSH computes SHA256 over the wire-encoded public key bytes (RFC 4253 format). The stored fingerprint will never match what ssh-keygen -lf displays. Operators who manually cross-check fingerprints will be confused and may make wrong trust decisions. Fix: Hash the DER-encoded public key bytes instead: const der publicKey.export({ type: spki, format: der }); const fingerprint createHash(sha256).update(der).digest(base64); --- [INFO] String zeroization is ineffective in JS session.js:192–194 if (typeof unlockedPrivateKey string) { unlockedPrivateKey ; } JS strings are immutable; setting the variable to does not overwrite the original string in memory. The ADRs acknowledge this limitation, but this is called out here because unlockProtectedPrivateKey returns a string specifically — it should return a Buffer so the caller can call .fill(0) on it. --- Logic Bugs [HIGH] promptText never prompts when a fallback is given resourceHelpers.js:15–21, called from run.js:51–52 export async function promptText(context, label, fallback, title) { if (fallback ! undefined fallback ! null fallback ! ) { return fallback; // ← always hits this for non-empty fallback } return context.prompt(label, title); } In buildExecutionSpec (run.js:51–52): const label await promptText(context, Temporary script name, adhoc-script, /run); const shell await promptText(context, Shell, sh, /run); Both calls pass non-empty hardcoded strings as fallback, so the user is never prompted — the ad-hoc script always gets named adhoc-script and always uses shell sh. This makes the ad-hoc form partially non-functional. Fix: Pass null as fallback and use the default as a hint in the label, or change promptText to pass the fallback as the default value shown to the user and always prompt: return context.prompt(label, title, { default: fallback }); --- [MEDIUM] shell value is injected into remote command string without quoting ssh/runtime.js:101–107 function buildRemoteCommand(script, runAs, shell sh) { const base ${shell} -lc ${shellQuote(script)}; ... } shell is interpolated verbatim. Once the promptText bug is fixed and users can actually enter shell values, a value like sh; malicious_command # would be injected into the remote command string. Since this string is eventually passed as the argument to ssh, the remote server would execute the injected command. Fix: Either validate shell against an allowlist ([sh, bash, zsh, ksh]) or shell-quote it: const base ${shellQuote(shell)} -lc ${shellQuote(script)}; --- [MEDIUM] SSH template setting silently removed on every startup without notice ssh/runtime.js:688–694 if (settings?.remove) { try { settings.remove(SSH_TEMPLATE_SETTING_KEY); } catch (_error) { } } If a user had a custom SSH template saved in settings (from an older CLI version that supported it), it is silently wiped on every startup without any migration notice. The user cannot tell why their custom template stopped working. Fix: Check if the setting exists first and emit a one-time migration warning to context.output before removing it. --- [LOW] Binding mismatch warning in /run batch is shown but does not abort run.js:273–282 const bindingRows await collectBindingMismatches(context, selectedServers); if (bindingRows.length 0) { ● Now I have a thorough picture of the codebase. Here is the full review. --- Code Review: devops_cli Architecture Assessment The overall design is solid. The vault sidecar boundary (ciphertext isolation), scrypt-based KEK derivation, atomic file writes, and fixed SSH backend strategy are all good engineering decisions. The plugin system and output block protocol are clean extension points. The main concerns below are specific bugs and hardening gaps, not architectural problems. --- Security Issues 1. unlockVaultIfNeeded doesnt catch vault errors — Logic Bug / UX Crash File: app/src/commands/resourceHelpers.js:40 await context.vaultSession.unlock(passcode); return true; If unlock() throws (wrong passcode, backoff locked), the error propagates unhandled up through the command layer. The function signature promises true/false but can throw instead. Any command calling this will surface a raw stack trace instead of a friendly error message. Fix: try { await context.vaultSession.unlock(passcode); return true; } catch (error) { context.output.push(error, Vault unlock failed: ${error.message}); return false; } --- 2. Double scrypt on every unlock — Performance / Unnecessary Exposure File: app/src/vault/session.js:180-181 unlockedPrivateKey unlockProtectedPrivateKey(authRecord, passcode); // scrypt #1 unwrappedMasterKey unwrapMasterEnvelope(description.masterEnvelope, passcode); // scrypt #2 unlockedPrivateKey is immediately zeroized in the finally block and never used. The auth private key is not yet in the critical path. This means every unlock runs scrypt twice (N131072 twice ≈ 2–4 seconds on typical hardware) for no benefit. Fix: Remove the unlockProtectedPrivateKey call from the unlock flow. Re-add it when the auth key is actually needed (future signing/binding use cases). --- 3. Plugin main path is not bounds-checked — Path Traversal File: app/src/plugins/loader.js:102 const mainPath path.resolve(entry.pluginDir, entry.manifest.main); if (!fs.existsSync(mainPath)) { ... } const mod await import(pathToFileURL(mainPath).href); A plugin can set main: ../../app/src/vault/sidecar.js and load arbitrary files outside its own directory. Plugins are currently local/trusted, but as the ecosystem grows (or if plugins are distributed), this is a low-cost hardening step. Fix: const mainPath path.resolve(entry.pluginDir, entry.manifest.main); const pluginDirNorm entry.pluginDir.endsWith(path.sep) ? entry.pluginDir : entry.pluginDir path.sep; if (!mainPath.startsWith(pluginDirNorm)) { // reject: path escapes plugin directory } --- 4. Host key fingerprint is not verified on Unix, only detected after-the-fact File: app/src/ssh/runtime.js:426-467 On Unix/WSL, the SSH command uses StrictHostKeyCheckingaccept-new. The host_key_fingerprint stored per server is never passed to ssh as a constraint — its only used post-execution to parse stderr and detect a changed key. This means: - First connection: silently auto-accepts any key - Subsequent connections: key changes raise an error, but only after the connection attempt On Windows (plink), -hostkey is correctly passed and verified before connecting. The Unix path has weaker guarantees. Fix (Unix): Pass -o FingerprintHashsha256 and use ControlMaster/known_hosts with the pinned fingerprint. Alternatively, use the VerifyHostKeyDNS approach, or at minimum, scan with ssh-keyscan first and compare before executing. --- 5. UNIENTS_SSH_SUDO_SECRET leaked into child process env — Unnecessary Exposure File: app/src/ssh/runtime.js:772-776 const env { ...process.env, UNIENTS_SSH_AUTH_SECRET: authSecret || , UNIENTS_SSH_JUMP_AUTH_SECRET: jumpAuthSecret || , UNIENTS_SSH_SUDO_SECRET: sudoPassword || // ← set but never consumed }; UNIENTS_SSH_AUTH_SECRET is consumed by the sshpass -e wrapper. UNIENTS_SSH_SUDO_SECRET is set in the local spawn env but nothing reads it — the sudo password is correctly passed via stdinData. This unnecessarily exposes the sudo password to the local SSH subprocess environment. Any child process of the SSH client can read it. Fix: Remove UNIENTS_SSH_SUDO_SECRET from the env object. --- 6. Non-standard fingerprint format in inspectPrivateKey — Interoperability Bug File: app/src/vault/crypto.js:312-314 const fingerprint createHash(sha256) .update(publicKeyPem, utf8) // ← SHA256 of the PEM string .digest(base64); return { ..., fingerprint: SHA256:${fingerprint} }; The SSH standard fingerprint is SHA256(base64_decode(wire_format_public_key)). This code hashes the PEM-formatted string (which includes headers and is base64 of DER, not the raw wire key). The resulting fingerprint will never match what ssh-keygen -l reports. This means the auth_material_fingerprint binding check in server records will always show mismatches if users try to cross-reference with external tools. Worse, two keys that produce the same fingerprint by this non-standard method would be considered equal. Fix: Use publicKeyOpenSsh output from ssh-keygen -l for fingerprinting, or compute it using the proper wire format: // Use ssh-keygen -l -f - for the stored OpenSSH pubkey to get the canonical fingerprint --- Logic Bugs 7. promptText always returns the default without prompting — UX Bug File: app/src/commands/resourceHelpers.js:15-21 and app/src/commands/run.js:51-57 export async function promptText(context, label, fallback, title) { if (fallback ! undefined fallback ! null fallback ! ) { return fallback; // ← returns immediately, never prompts } return context.prompt(label, title); } In run.js: const label await promptText(context, Temporary script name, adhoc-script, /run); const shell await promptText(context, Shell, sh, /run); const timeoutInput await promptText(context, Timeout seconds, 60, /run); All three calls pass non-empty fallbacks, so the user is never prompted for script name, shell, or timeout. The defaults are always used silently. The user can only provide the script content. Fix: The intent of promptText appears to be prompt the user, pre-filling a default. The implementation should call context.prompt with the default as a hint, not skip the prompt entirely. Either fix the helper or call context.prompt directly. --- 8. Credentials materialized before user confirms execution File: app/src/commands/run.js:284-307 const credentialState await materializeCredentials(context, selectedServers, spec); // ... show failure table ... const confirmed await context.confirm(Start batch execution for ${N} server(s)?, /run); All credentials (SSH keys, passwords) are decrypted from the vault and held in plaintext strings before the user confirms the run. If the user cancels, the credentials are still in memory until GC. These should be materialized after confirmation. --- 9. Batch run continues with partial credential failures, no re-confirmation File: app/src/commands/run.js:289-300 if (failures.size 0) { context.output.pushRich({ kind: table, type: error, ... }); } if (credentials.size 0) { context.output.push(error, Unable to read credentials for any selected server.); return; } // Falls through to confirmation with partial server set If 5 out of 10 servers fail credential materialization, the code shows the failure table then proceeds to the confirmation. The confirm dialog says Start batch execution for 10 server(s)? but only 5 will actually run. The user may not notice the discrepancy. Fix: Either abort if any credential fails, or adjust the confirmation message to say X of Y servers ready, N will be skipped. --- 10. Pre-flight credential ref check doesnt verify vault existence File: app/src/commands/run.js:261-271 and app/src/commands/resourceHelpers.js (implied) buildMissingCredentialRows only checks that auth_secret_ref is non-null. It doesnt verify the referenced name actually exists in the vault. The real check happens during materializeCredentials. So the pre-flight check passes but execution still fails — just later with a less clear error. --- Extensibility Gaps 11. No plugin API versioning createPlugin() currently takes no arguments. When the framework adds capabilities (e.g., new context methods, new output block types), plugins have no way to declare which API version they require. A v2 plugin loaded in a v1 CLI (or vice versa) fails silently with a runtime error, not a clear version mismatch message. Suggestion: Pass a { apiVersion, context } object to createPlugin(), and add a minApiVersion/maxApiVersion field to plugin.json. The loader can reject version-incompatible plugins with a clear message. --- 12. No structured error taxonomy All errors are new Error(human message). Commands, plugins, and the SSH runtime use different error detection strategies (string matching on result.errorType, try/catch of generic Error). As the plugin ecosystem grows, plugins need a reliable way to handle vault locked vs SSH timeout vs server not found without parsing error message strings. Suggestion: Define an error class hierarchy or at minimum a set of error code constants: export const ErrorCode { VAULT_LOCKED: VAULT_LOCKED, VAULT_NOT_INITIALIZED: VAULT_NOT_INITIALIZED, SSH_TIMEOUT: SSH_TIMEOUT, SSH_HOST_KEY_CHANGED: SSH_HOST_KEY_CHANGED, CREDENTIAL_NOT_FOUND: CREDENTIAL_NOT_FOUND }; --- 13. Command dispatch is entirely flat Each command handles its own subcommand parsing (if (first batch)). Theres no standard routing tree, no shared unknown subcommand error message, and tab completion has to be implemented per-command. As the number of commands grows (especially with plugins), this becomes inconsistent. Suggestion: A simple command router factory would help: createCommand(/run, { batch: runBatch, template: runTemplate }); This also makes suggest() implementable generically from the subcommand keys. --- Minor / Code Quality 14. Duplicate tightenPrivateKeyFilePermissions / tightenIdentityFilePermissions crypto.js and ssh/runtime.js both contain near-identical icacls permission-tightening logic. Extract to utils/fileSecurity.js. 15. isUnlocked() and remainingMs() have an inconsistency isUnlocked() calls zeroize() as a side effect when the session has expired. remainingMs() calls isUnlocked() for its check. This means remainingMs() has a side-effectful path thats not obvious from the name. 16. buildMissingCredentialRows and collectBindingMismatches both call resolveManagedJumpServer per server During runBatch, jump server resolution is called 3 separate times per server (in pre-flight, binding check, and during execution). These could be cached per-server for the duration of the run. 17. Vault sidecar vault.reset deletes the store without requiring confirmation File: app/src/vault/sidecar.js:68-72 The sidecars vault.reset handler unconditionally deletes store.json. If the CLI-side code calls this in a failed init rollback (session.js:151), thats correct. But its also callable directly — theres no guard that prevents a mistaken reset from destroying all vault data. --- Summary Table ┌─────┬─────────────────────────────────────────────────────┬───────────────┬────────────────────────┐ │ # │ Issue │ Severity │ File │ ├─────┼─────────────────────────────────────────────────────┼───────────────┼────────────────────────┤ │ 1 │ unlockVaultIfNeeded unhandled throw │ High │ resourceHelpers.js:40 │ ├─────┼─────────────────────────────────────────────────────┼───────────────┼────────────────────────┤ │ 2 │ Double scrypt in unlock() │ Medium │ session.js:180 │ ├─────┼─────────────────────────────────────────────────────┼───────────────┼────────────────────────┤ │ 3 │ Plugin main path traversal │ Medium │ loader.js:102 │ ├─────┼─────────────────────────────────────────────────────┼───────────────┼────────────────────────┤ │ 4 │ Host key not verified on Unix │ Medium │ runtime.js:438 │ ├─────┼─────────────────────────────────────────────────────┼───────────────┼────────────────────────┤ │ 5 │ UNIENTS_SSH_SUDO_SECRET leaked to child env │ Medium │ runtime.js:775 │ ├─────┼─────────────────────────────────────────────────────┼───────────────┼────────────────────────┤ │ 6 │ Non-standard fingerprint in inspectPrivateKey │ Medium │ crypto.js:312 │ ├─────┼─────────────────────────────────────────────────────┼───────────────┼────────────────────────┤ │ 7 │ promptText never prompts when default given │ Medium │ resourceHelpers.js:15 │ ├─────┼─────────────────────────────────────────────────────┼───────────────┼────────────────────────┤ │ 8 │ Credentials materialized before confirmation │ Low │ run.js:284 │ ├─────┼─────────────────────────────────────────────────────┼───────────────┼────────────────────────┤ │ 9 │ Batch continues with partial failures silently │ Low │ run.js:289 │ ├─────┼─────────────────────────────────────────────────────┼───────────────┼────────────────────────┤ │ 10 │ Pre-flight ref check doesnt verify vault existence │ Low │ run.js:261 │ ├─────┼─────────────────────────────────────────────────────┼───────────────┼────────────────────────┤ │ 11 │ No plugin API versioning │ Extensibility │ loader.js │ ├─────┼─────────────────────────────────────────────────────┼───────────────┼────────────────────────┤ │ 12 │ No structured error taxonomy │ Extensibility │ all │ ├─────┼─────────────────────────────────────────────────────┼───────────────┼────────────────────────┤ │ 13 │ Flat command dispatch │ Extensibility │ commandDispatcher │ ├─────┼─────────────────────────────────────────────────────┼───────────────┼────────────────────────┤ │ 14 │ Duplicate permission-tightening code │ Quality │ crypto.js / runtime.js │ ├─────┼─────────────────────────────────────────────────────┼───────────────┼────────────────────────┤ │ 15 │ isUnlocked() has zeroize side effect │ Quality │ session.js │ └─────┴─────────────────────────────────────────────────────┴───────────────┴────────────────────────┘ The most important fixes to do first are #1 (crash on wrong passcode), #6 (fingerprint format — affects binding integrity), #7 (users cant set shell/timeout for ad-hoc runs), and #3 (plugin path bounds). ✻ Cogitated for 7m 38s ❯ /model ⎿ Set model to Opus 4.6 ❯ 2. Double scrypt on every unlock — Performance / Unnecessary Exposure File: app/src/vault/session.js:180-181 unlockedPrivateKey unlockProtectedPrivateKey(authRecord, passcode); // scrypt #1 unwrappedMasterKey unwrapMasterEnvelope(description.masterEnvelope, passcode); // scrypt #2 unlockedPrivateKey is immediately zeroized in the finally block and never used. The auth private key is not yet in the critical path. This means every unlock runs scrypt twice (N131072 twice ≈ 2–4 seconds on typical hardware) for no benefit. Fix: Remove the unlockProtectedPrivateKey call from the unlock flow. Re-add it when the auth key is actually needed (future signing/binding use cases). About this, the first ones purpose is to valid the passcode is correct. ● Good point, I understand the intent now. The unlockProtectedPrivateKey call serves as an independent passcode verification against a separate encrypted artifact (the auth key, stored in a different file from the master envelope). If one file is tampered with or corrupted but the other isnt, the mismatch surfaces early. That said, unwrapMasterEnvelope alone already validates the passcode — AES-256-GCMs auth tag will reject a wrong key. So the two-check design provides cross-file tamper detection, not just passcode validation. If thats the goal, its worth keeping but should be documented explicitly in a comment, because the cost is real: 2x scrypt at N131072 on every unlock. A couple of alternatives worth considering if the 2x cost becomes a concern: 1. Cheaper cross-check: Store a passcode verification hash (e.g., HMAC of a known constant using the derived KEK) alongside the auth key file. Check that first (fast), then do the single scrypt for the master envelope. Same tamper detection, one scrypt instead of two. 2. Defer to when auth key is actually needed: When the auth key enters the critical path (future signing/binding), the double scrypt becomes justified because both decrypted values are used. Until then, the cross-validation benefit is modest given that ADR-0001 already acknowledges file replacement attacks arent fully mitigated. Either way, the intent should be captured in a comment at that call site — its non-obvious that the first decryption is a deliberate integrity check rather than dead code. ✻ Worked for 31s ❯ 2x cost isnt a matter ,since only critical path needs verifing the passcode, and the state will be cached for 30 minites default. ● Makes sense. The unlock is a rare operation (once per 30-minute session), so 2x scrypt cost is negligible in practice. Ill downgrade that finding — its working as intended. ❯ /usage ⎿ Status dialog dismissed ❯ /usage ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── Status Config Usage Stats Current session ██████████████▌ 29% used Resets 4pm (Asia/Shanghai) Current week (all models) ███████████████████████████████████▌ 71% used Resets Apr 10, 6pm (Asia/Shanghai) Extra usage Extra usage not enabled · /extra-usage to enable Esc to cancel

更多文章