From 62f0d7062e268966ce9377d8606660eb24d88d3f Mon Sep 17 00:00:00 2001 From: Malkovich-666 <110989971+Malkovich-666@users.noreply.github.com> Date: Fri, 17 Apr 2026 17:00:17 +0800 Subject: [PATCH 1/6] feat: add anti-track module --- crates/api/src/anti_track.rs | 170 +++++++++++++++++++++++++++++++++++ 1 file changed, 170 insertions(+) create mode 100644 crates/api/src/anti_track.rs diff --git a/crates/api/src/anti_track.rs b/crates/api/src/anti_track.rs new file mode 100644 index 0000000..40f554f --- /dev/null +++ b/crates/api/src/anti_track.rs @@ -0,0 +1,170 @@ +//! Anti-Tracking and Anti-Upgrade Module +//! +//! Provides mechanisms to: +//! - Disable automatic version checks and updates +//! - Remove/block telemetry and analytics requests +//! - Sanitize network requests to avoid detection +//! +//! This module is inspired by the anti-track implementation in +//! [rusty-ai-cli](https://github.com/lorryjovens-hub/claude-code-rust), +//! contributed with assistance from the Hermes Agent. + +use std::sync::LazyLock; + +/// Anti-tracking configuration. +#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)] +#[serde(default, rename_all = "camelCase")] +pub struct AntiTrackConfig { + /// Disable version check requests. + pub disable_version_check: bool, + /// Disable telemetry/analytics. + pub disable_telemetry: bool, + /// Disable update checks. + pub disable_update_check: bool, + /// Block known tracking domains. + pub block_tracking_domains: bool, + /// Use fake version identifiers. + pub spoof_version: bool, + /// Spoofed version string. + pub spoofed_version: String, + /// Disable crash reporting. + pub disable_crash_reports: bool, + /// Disable feedback submission. + pub disable_feedback: bool, +} + +impl AntiTrackConfig { + /// Create a new config with all protections enabled. + #[must_use] + pub fn enabled() -> Self { + Self { + disable_version_check: true, + disable_telemetry: true, + disable_update_check: true, + block_tracking_domains: true, + spoof_version: true, + spoofed_version: "1.0.0".to_string(), + disable_crash_reports: true, + disable_feedback: true, + } + } +} + +/// Domains known to be used for tracking/telemetry. +pub static TRACKING_DOMAINS: LazyLock> = LazyLock::new(|| { + vec![ + "telemetry.anthropic.com", + "analytics.anthropic.com", + "client.telemetry.github.com", + "api.segment.io", + "events.devoptix.com", + "litestream.io", + "ipfs.io", + "cloudflare-ipfs.com", + ] +}); + +/// Sensitive header names that reveal identity or enable tracking. +pub static SENSITIVE_HEADERS: LazyLock> = LazyLock::new(|| { + vec![ + "X-Claude-Client-Name", + "X-Claude-Client-Version", + "X-Anthropic-Telemetry", + "X-Analytics", + "X-Tracking-ID", + "X-Session-ID", + "X-User-ID", + ] +}); + +/// Check if a URL should be blocked based on anti-track rules. +#[must_use] +pub fn should_block_url(url: &str, config: &AntiTrackConfig) -> bool { + if config.block_tracking_domains && TRACKING_DOMAINS.iter().any(|d| url.contains(d)) { + return true; + } + + if config.disable_version_check && (url.contains("/version") || url.contains("/update") || url.contains("/check")) { + return true; + } + + false +} + +/// Sanitize a `reqwest::Request` by removing tracking headers. +pub fn sanitize_request(request: &mut reqwest::Request, config: &AntiTrackConfig) { + let headers = request.headers_mut(); + + // Remove sensitive headers. + for name in SENSITIVE_HEADERS.iter() { + if let Ok(h) = name.parse::() { + headers.remove(h); + } + } + + // Remove any header containing telemetry/analytics/tracking keywords. + let to_remove: Vec = headers + .keys() + .filter(|k| { + let lower = k.as_str().to_lowercase(); + lower.contains("telemetry") + || lower.contains("analytics") + || lower.contains("tracking") + || lower.contains("crash") + || lower.contains("feedback") + }) + .cloned() + .collect(); + + for key in to_remove { + headers.remove(key); + } + + // Spoof User-Agent if enabled. + if config.spoof_version { + if let Ok(ua) = reqwest::header::HeaderValue::from_str(&format!( + "CrabCode/{} (Rust CLI)", + config.spoofed_version + )) { + headers.insert(reqwest::header::USER_AGENT, ua); + } + } +} + +/// Convenience wrapper that returns `None` when the request should be blocked. +pub fn apply(request: reqwest::Request, config: &AntiTrackConfig) -> Option { + if should_block_url(request.url().as_str(), config) { + return None; + } + let mut request = request; + sanitize_request(&mut request, config); + Some(request) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_block_tracking_domain() { + let config = AntiTrackConfig::enabled(); + assert!(should_block_url("https://telemetry.anthropic.com/v1/track", &config)); + assert!(!should_block_url("https://api.anthropic.com/v1/messages", &config)); + } + + #[test] + fn test_sanitize_request() { + let config = AntiTrackConfig::enabled(); + let mut request = reqwest::Request::new( + reqwest::Method::GET, + "https://api.anthropic.com/v1/models".parse().unwrap(), + ); + request.headers_mut().insert("X-Claude-Client-Version", "1.0.0".parse().unwrap()); + request.headers_mut().insert("Authorization", "Bearer token".parse().unwrap()); + + sanitize_request(&mut request, &config); + + assert!(!request.headers().contains_key("X-Claude-Client-Version")); + assert!(request.headers().contains_key("Authorization")); + } +} From 6e8eed0fe7eb5108cc7dfff509d822a7b8a2a3cb Mon Sep 17 00:00:00 2001 From: Malkovich-666 <110989971+Malkovich-666@users.noreply.github.com> Date: Fri, 17 Apr 2026 17:06:30 +0800 Subject: [PATCH 2/6] feat: add anti_track module declaration and wire into create_backend --- crates/api/src/lib.rs | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/crates/api/src/lib.rs b/crates/api/src/lib.rs index b2ee476..27076f2 100644 --- a/crates/api/src/lib.rs +++ b/crates/api/src/lib.rs @@ -5,6 +5,7 @@ //! No dynamic trait dispatch; compile-time determined, exhaustive match. pub mod anthropic; +pub mod anti_track; pub mod cache; pub mod capabilities; pub mod context_optimizer; @@ -136,6 +137,9 @@ impl LlmBackend { /// - Everything else (including `None`) → `AnthropicClient` #[must_use] pub fn create_backend(settings: &crab_config::Settings) -> LlmBackend { + // Extract anti-track config from settings + let anti_track = settings.anti_track.clone(); + match settings.api_provider.as_deref() { Some("openai" | "ollama" | "deepseek" | "vllm") => { let base_url = settings @@ -146,7 +150,7 @@ pub fn create_backend(settings: &crab_config::Settings) -> LlmBackend { .api_key .clone() .or_else(|| std::env::var("OPENAI_API_KEY").ok()); - LlmBackend::OpenAi(openai::OpenAiClient::new(base_url, api_key)) + LlmBackend::OpenAi(openai::OpenAiClient::new_with_anti_track(base_url, api_key, anti_track)) } #[cfg(feature = "bedrock")] Some("bedrock") => { @@ -169,9 +173,10 @@ pub fn create_backend(settings: &crab_config::Settings) -> LlmBackend { |_| { // Fall back to direct Anthropic if Bedrock auth fails let auth = crab_auth::create_auth_provider(settings); - LlmBackend::Anthropic(anthropic::AnthropicClient::new( + LlmBackend::Anthropic(anthropic::AnthropicClient::new_with_anti_track( "https://api.anthropic.com", auth, + anti_track, )) }, LlmBackend::Bedrock, @@ -200,9 +205,10 @@ pub fn create_backend(settings: &crab_config::Settings) -> LlmBackend { vertex::create_vertex_client(&config).map_or_else( |_| { let auth = crab_auth::create_auth_provider(settings); - LlmBackend::Anthropic(anthropic::AnthropicClient::new( + LlmBackend::Anthropic(anthropic::AnthropicClient::new_with_anti_track( "https://api.anthropic.com", auth, + anti_track, )) }, LlmBackend::Vertex, @@ -214,7 +220,7 @@ pub fn create_backend(settings: &crab_config::Settings) -> LlmBackend { .as_deref() .unwrap_or("https://api.anthropic.com"); let auth = crab_auth::create_auth_provider(settings); - LlmBackend::Anthropic(anthropic::AnthropicClient::new(base_url, auth)) + LlmBackend::Anthropic(anthropic::AnthropicClient::new_with_anti_track(base_url, auth, anti_track)) } } } From 91e9ae8c75172ee32e60a4016485a00663a172b2 Mon Sep 17 00:00:00 2001 From: Malkovich-666 <110989971+Malkovich-666@users.noreply.github.com> Date: Fri, 17 Apr 2026 17:09:45 +0800 Subject: [PATCH 3/6] feat: wire anti-track into anthropic client --- crates/api/src/anthropic/client.rs | 31 +++++++++++++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/crates/api/src/anthropic/client.rs b/crates/api/src/anthropic/client.rs index 27f5c3c..df245c9 100644 --- a/crates/api/src/anthropic/client.rs +++ b/crates/api/src/anthropic/client.rs @@ -16,10 +16,19 @@ pub struct AnthropicClient { http: reqwest::Client, base_url: String, auth: Box, + anti_track: Option, } impl AnthropicClient { pub fn new(base_url: &str, auth: Box) -> Self { + Self::new_with_anti_track(base_url, auth, None) + } + + pub fn new_with_anti_track( + base_url: &str, + auth: Box, + anti_track: Option, + ) -> Self { let http = reqwest::Client::builder() .timeout(std::time::Duration::from_secs(300)) .pool_max_idle_per_host(4) @@ -30,6 +39,7 @@ impl AnthropicClient { http, base_url: base_url.to_string(), auth, + anti_track, } } @@ -57,6 +67,21 @@ impl AnthropicClient { Ok(builder) } + /// Apply anti-track sanitization to a request before sending. + fn apply_anti_track(&self, builder: reqwest::RequestBuilder) -> reqwest::RequestBuilder { + if let Some(ref config) = self.anti_track { + match builder.build() { + Ok(mut request) => { + crate::anti_track::sanitize_request(&mut request, config); + self.http.request(request.method().clone(), request.url().clone()).headers(request.headers().clone()).body(request.body().cloned().unwrap_or_default()) + } + Err(_) => builder, + } + } else { + builder + } + } + /// Streaming call — POST `/v1/messages` with `stream: true`. /// /// Returns a stream of `StreamEvent` mapped from Anthropic SSE events. @@ -70,6 +95,7 @@ impl AnthropicClient { futures::stream::once(async move { let body = serde_json::to_vec(&api_req).map_err(ApiError::Json)?; let request = self.build_request(&body).await?; + let request = self.apply_anti_track(request); let response = request.send().await.map_err(ApiError::Http)?; let status = response.status(); @@ -115,6 +141,7 @@ impl AnthropicClient { let api_req = convert::to_anthropic_request(&req, false); let body = serde_json::to_vec(&api_req).map_err(ApiError::Json)?; let request = self.build_request(&body).await?; + let request = self.apply_anti_track(request); let response = request.send().await.map_err(ApiError::Http)?; let status = response.status(); @@ -162,6 +189,7 @@ impl AnthropicClient { } } + let builder = self.apply_anti_track(builder); let response = builder.send().await.map_err(ApiError::Http)?; let status = response.status(); if !status.is_success() { @@ -197,7 +225,7 @@ impl AnthropicClient { Ok(models) } - /// Health check — verify the API is reachable and the key is valid. + /// Health check — verify the API is reachable and the API key is valid. /// /// Sends a minimal request to validate connectivity and authentication. pub async fn health_check(&self) -> crate::capabilities::HealthStatus { @@ -232,6 +260,7 @@ impl AnthropicClient { } } + let builder = self.apply_anti_track(builder); match builder.send().await { Ok(resp) => { let status = resp.status(); From 39b14b89608c690678bb0057d2346ffcb44f3024 Mon Sep 17 00:00:00 2001 From: Malkovich-666 <110989971+Malkovich-666@users.noreply.github.com> Date: Fri, 17 Apr 2026 17:29:41 +0800 Subject: [PATCH 4/6] feat: wire anti-track into openai client --- crates/api/src/openai/client.rs | 31 +++++++++++++++++++++++++++++-- 1 file changed, 29 insertions(+), 2 deletions(-) diff --git a/crates/api/src/openai/client.rs b/crates/api/src/openai/client.rs index c28320a..f4b71e1 100644 --- a/crates/api/src/openai/client.rs +++ b/crates/api/src/openai/client.rs @@ -15,10 +15,19 @@ pub struct OpenAiClient { http: reqwest::Client, base_url: String, api_key: Option, + anti_track: Option, } impl OpenAiClient { pub fn new(base_url: &str, api_key: Option) -> Self { + Self::new_with_anti_track(base_url, api_key, None) + } + + pub fn new_with_anti_track( + base_url: &str, + api_key: Option, + anti_track: Option, + ) -> Self { let http = reqwest::Client::builder() .timeout(std::time::Duration::from_secs(300)) .pool_max_idle_per_host(4) @@ -29,6 +38,22 @@ impl OpenAiClient { http, base_url: base_url.trim_end_matches('/').to_string(), api_key, + anti_track, + } + } + + /// Apply anti-track sanitization to a request before sending. + fn apply_anti_track(&self, builder: reqwest::RequestBuilder) -> reqwest::RequestBuilder { + if let Some(ref config) = self.anti_track { + match builder.build() { + Ok(mut request) => { + crate::anti_track::sanitize_request(&mut request, config); + self.http.request(request.method().clone(), request.url().clone()).headers(request.headers().clone()).body(request.body().cloned().unwrap_or_default()) + } + Err(_) => builder, + } + } else { + builder } } @@ -60,7 +85,7 @@ impl OpenAiClient { // We need to use stream::once + flatten to handle the async request // setup followed by the streaming response. stream::once(async move { - let resp = self.build_request(&chat_req).send().await.map_err(|e| { + let resp = self.apply_anti_track(self.build_request(&chat_req)).send().await.map_err(|e| { if e.is_timeout() { ApiError::Timeout } else { @@ -93,7 +118,7 @@ impl OpenAiClient { pub async fn send(&self, req: MessageRequest<'_>) -> Result { let chat_req = convert::to_chat_completion_request(&req, false); - let resp = self.build_request(&chat_req).send().await.map_err(|e| { + let resp = self.apply_anti_track(self.build_request(&chat_req)).send().await.map_err(|e| { if e.is_timeout() { ApiError::Timeout } else { @@ -130,6 +155,7 @@ impl OpenAiClient { builder = builder.bearer_auth(key); } + let builder = self.apply_anti_track(builder); let response = builder.send().await.map_err(|e| { if e.is_timeout() { crate::error::ApiError::Timeout @@ -184,6 +210,7 @@ impl OpenAiClient { builder = builder.bearer_auth(key); } + let builder = self.apply_anti_track(builder); match builder.send().await { Ok(resp) => { let status = resp.status(); From 8886577ab7f445334483fa022b8cd601f0ff6d7f Mon Sep 17 00:00:00 2001 From: Malkovich-666 <110989971+Malkovich-666@users.noreply.github.com> Date: Fri, 17 Apr 2026 17:32:15 +0800 Subject: [PATCH 5/6] feat: add AntiTrackConfig to settings --- crates/config/src/settings.rs | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/crates/config/src/settings.rs b/crates/config/src/settings.rs index 8784937..fe99871 100644 --- a/crates/config/src/settings.rs +++ b/crates/config/src/settings.rs @@ -8,6 +8,28 @@ const CONFIG_DIR: &str = ".crab"; /// Settings file name within config directories. const SETTINGS_FILE: &str = "settings.json"; +/// Anti-tracking and anti-upgrade configuration. +#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)] +#[serde(default, rename_all = "camelCase")] +pub struct AntiTrackConfig { + /// Disable version check requests. + pub disable_version_check: bool, + /// Disable telemetry/analytics. + pub disable_telemetry: bool, + /// Disable update checks. + pub disable_update_check: bool, + /// Block known tracking domains. + pub block_tracking_domains: bool, + /// Use fake version identifiers. + pub spoof_version: bool, + /// Spoofed version string. + pub spoofed_version: String, + /// Disable crash reporting. + pub disable_crash_reports: bool, + /// Disable feedback submission. + pub disable_feedback: bool, +} + /// Application settings, loaded from `~/.crab/settings.json` (global) /// and `.crab/settings.json` (project-level). /// @@ -31,6 +53,8 @@ pub struct Settings { /// Environment variables to inject into the process. /// CC-compatible: `{"env": {"ANTHROPIC_API_KEY": "sk-ant-xxx"}}`. pub env: Option>, + /// Anti-tracking and anti-upgrade settings. + pub anti_track: Option, } /// Configuration for git context injection into system prompts. @@ -80,6 +104,7 @@ impl Settings { (Some(base), None) => Some(base.clone()), (None, None) => None, }, + anti_track: other.anti_track.clone().or(self.anti_track), } } } From 21350c094b962c07f02107cf3cdc8c91084e5a94 Mon Sep 17 00:00:00 2001 From: Malkovich-666 <110989971+Malkovich-666@users.noreply.github.com> Date: Fri, 17 Apr 2026 17:33:19 +0800 Subject: [PATCH 6/6] feat: refine anti_track module to use crab_config::AntiTrackConfig --- crates/api/src/anti_track.rs | 49 +++++++----------------------------- 1 file changed, 9 insertions(+), 40 deletions(-) diff --git a/crates/api/src/anti_track.rs b/crates/api/src/anti_track.rs index 40f554f..f226e48 100644 --- a/crates/api/src/anti_track.rs +++ b/crates/api/src/anti_track.rs @@ -11,44 +11,7 @@ use std::sync::LazyLock; -/// Anti-tracking configuration. -#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)] -#[serde(default, rename_all = "camelCase")] -pub struct AntiTrackConfig { - /// Disable version check requests. - pub disable_version_check: bool, - /// Disable telemetry/analytics. - pub disable_telemetry: bool, - /// Disable update checks. - pub disable_update_check: bool, - /// Block known tracking domains. - pub block_tracking_domains: bool, - /// Use fake version identifiers. - pub spoof_version: bool, - /// Spoofed version string. - pub spoofed_version: String, - /// Disable crash reporting. - pub disable_crash_reports: bool, - /// Disable feedback submission. - pub disable_feedback: bool, -} - -impl AntiTrackConfig { - /// Create a new config with all protections enabled. - #[must_use] - pub fn enabled() -> Self { - Self { - disable_version_check: true, - disable_telemetry: true, - disable_update_check: true, - block_tracking_domains: true, - spoof_version: true, - spoofed_version: "1.0.0".to_string(), - disable_crash_reports: true, - disable_feedback: true, - } - } -} +pub use crab_config::AntiTrackConfig; /// Domains known to be used for tracking/telemetry. pub static TRACKING_DOMAINS: LazyLock> = LazyLock::new(|| { @@ -147,14 +110,20 @@ mod tests { #[test] fn test_block_tracking_domain() { - let config = AntiTrackConfig::enabled(); + let config = AntiTrackConfig { + block_tracking_domains: true, + ..Default::default() + }; assert!(should_block_url("https://telemetry.anthropic.com/v1/track", &config)); assert!(!should_block_url("https://api.anthropic.com/v1/messages", &config)); } #[test] fn test_sanitize_request() { - let config = AntiTrackConfig::enabled(); + let config = AntiTrackConfig { + spoof_version: false, + ..Default::default() + }; let mut request = reqwest::Request::new( reqwest::Method::GET, "https://api.anthropic.com/v1/models".parse().unwrap(),