Is ClearTok - TikTok Repost Remover safe?
ClearTok sends your TikTok username and repost-removal activity to its own servers via a session-based tracking system.
Each time you use the extension, it creates a session tied to your TikTok username and reports events — including when you start and complete a repost-removal run, how many reposts were removed, and any errors encountered — to api.cleartok.io. Removal counts are transmitted with your cleartok.io account cookies attached, linking your usage to your account. The extension also fetches its DOM selector configuration remotely every six hours, allowing the operator to update which buttons the extension clicks on TikTok without a new extension release.
Who publishes itPersonal - no other listings under this identity
Personal - no other listings under this identity
What this publisher told the store about itself, and the other listings that told it the same thing.
AI-generated. Findings may contain errors. Those marked Verified have been manually reviewed.
Publishers can request a review.
Findings
Remote selector updates change TikTok DOM actions
ClearTok requests selector metadata from api.cleartok.io and gets a selectors object, stored locally to find TikTok profile, repost-tab, repost-button, and video-list elements.
Refreshed on install, startup, and after six hours.
- Severity
- Medium unwanted
- Type
- Unexpected
- CWE
- CWE-829
- Source
- Dynamic sandbox
The extension reaches a selector refresh point.
This happens on install, browser startup, or after the stored selector metadata is older than six hours.
It downloads selector metadata and applies the returned DOM selectors to the TikTok workflow.
The selectors determine which profile, repost-tab, repost-list, and repost-button elements the content script searches for.
200 OK JSON containing "version":"1.0.6" and a top-level "selectors" object.
- Selector version1.0.6
Lets the extension decide whether to replace the selectors already stored on your device.
- Profile navigation selectors[data-e2e="nav-profile"], [data-e2e="repost-tab"], a[href*="repost"]
Controls how the extension finds your TikTok profile and the Reposts tab before taking action.
- Repost list selectors[data-e2e="user-repost-item-list"], [data-e2e="user-repost-item"]
Controls which TikTok page elements the extension treats as reposted videos or photos.
- Repost button selectors[data-e2e="video-share-repost"], button[aria-label*="repost" i]
Controls which on-page button the extension looks for when removing a repost.
The selector set lives in extension storage and can change which TikTok page elements are clicked during the removal workflow.
- Location
- chrome.storage.local keys 'selectors' and 'selectorsMeta'
{ "selectors": { "video": { "repostItem": "[data-e2e=\"user-repost-item\"]", "repostButton": [ "[data-e2e=\"video-share-repost\"]", "[data-e2e=\"repost-action-tag\"]", "button[aria-label*=\"repost\" i]", "[class*=\"repost\"]" ], "repostListContainer": "[data-e2e=\"user-repost-item-list\"]" }, "navigation": { "repostTab": [ "[data-e2e=\"repost-tab\"]", "[class*=\"PRepost\"]", "a[href*=\"repost\"]", "button[data-testid=\"repost-tab\"]" ], "navProfileLink": "[data-e2e=\"nav-profile\"]", "editProfileButton": "[data-e2e=\"edit-profile-entrance\"]" } }, "selectorsMeta": { "version": "1.0.6", "fetchedAt": 1783840904922 }}The fetch, storage write, and runtime reload path
Background selector refresh
background.jsconst META_URL = 'https://api.cleartok.io/cdn/selectors';const SELECTOR_CACHE_TTL = 1000 * 60 * 60 * 6; // 6 hoursasync function checkMetaAndUpdate(force = false) { const { selectorsMeta } = await chrome.storage.local.get('selectorsMeta'); if (!force && selectorsMeta && Date.now() - selectorsMeta.fetchedAt < SELECTOR_CACHE_TTL) { return; } try { const res = await fetch(META_URL, { cache: 'no-cache' }); if (!res.ok) return; const meta = await res.json(); if (!meta?.version || !meta?.selectors) { console.warn('[ClearTok BG] Invalid meta file'); return; } if (selectorsMeta?.version === meta.version) { console.log('[ClearTok BG] Selectors up-to-date'); return; } await chrome.storage.local.set({ selectors: meta.selectors, selectorsMeta: { version: meta.version, fetchedAt: Date.now() } }); console.log('[ClearTok BG] Selectors updated to version:', meta.version); // 只通知正在处理的标签页(如果有) if (stateManager.state.process.tabId) { chrome.tabs.sendMessage(stateManager.state.process.tabId, { type: 'SELECTORS_UPDATED', timestamp: Date.now() }).catch(() => { }); } } catch (e) { console.warn('[ClearTok BG] Failed to fetch selectors:', e); }}chrome.runtime.onStartup?.addListener(() => checkMetaAndUpdate(true));chrome.runtime.onInstalled.addListener(() => { console.log("[ClearTok BG] Extension installed"); checkMetaAndUpdate(true); chrome.sidePanel.setPanelBehavior({ openPanelOnActionClick: true });});Content-script selector loading
modules/config.jsasync init() { // 开发模式:跳过缓存,直接加载 fallback if (!ConfigManager.FORCE_RELOAD_SELECTORS) { try { const { selectors } = await chrome.storage.local.get('selectors'); if (selectors && Object.keys(selectors).length > 0) { const missing = ConfigManager._missingRequiredKeys(selectors); if (missing.length === 0) { this.selectors = selectors; console.log('[ClearTok] Selectors loaded from storage.'); this.listenForUpdates(); return; } // 缓存陈旧:缺失新版必需 key —— 不使用,fall through 到 fallback console.warn( '[ClearTok] Cached selectors are stale (missing keys:', missing.join(', '), '). Reloading from bundled fallback JSON.' ); } } catch (error) { console.error('[ClearTok] Error loading selectors from storage:', error); } } else { console.log('[ClearTok] FORCE_RELOAD_SELECTORS is ON, loading from fallback...'); } // 从后备文件加载 const fallbackUrl = chrome.runtime.getURL('assets/selectors-fallback.json'); try { const res = await fetch(fallbackUrl); const json = await res.json(); this.selectors = json.selectors; console.log('[ClearTok] Selectors loaded from fallback JSON (version ' + (json.version || '?') + ').'); // 用 fallback 覆写缓存,避免下次又被陈旧缓存复活 try { await chrome.storage.local.set({ selectors: json.selectors, selectorsMeta: { version: json.version || 'fallback', fetchedAt: Date.now(), source: 'fallback' } }); } catch (_) { /* 非关键 */ } } catch (error) { console.error('[ClearTok] Failed to load fallback selectors:', error); } this.listenForUpdates();}listenForUpdates() { chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => { // 使用新的消息格式 if (msg.type === 'SELECTORS_UPDATED') { chrome.storage.local.get('selectors').then(({ selectors }) => { if (selectors) { const missing = ConfigManager._missingRequiredKeys(selectors); if (missing.length === 0) { this.selectors = selectors; console.log('[ClearTok] ✨ Selectors hot-reloaded!'); } else { console.warn( '[ClearTok] Ignored SELECTORS_UPDATED — incoming selectors missing required keys:', missing.join(', ') ); } } // 确认消息已处理 sendResponse({ success: true, result: 'reloaded selectors' }); }); // 保持通道开放以进行异步响应 return true; } });}- api.cleartok.io
Receives the selector-metadata request and returns JSON that the extension stores for TikTok DOM element matching.
TikTok workflow events sent to ClearTok API
ClearTok sends event payloads to api.cleartok.io during repost removal, which can include your TikTok username, account ID, premium status, repost counts, totals, and page URL.
A PUT was observed; only a session-init payload was captured.
- Severity
- Medium unwanted
- Type
- Unexpected
- CWE
- CWE-359
- Source
- Dynamic sandbox
You start or use the TikTok repost-removal workflow.
Login checks, progress updates, workflow completion, errors, and element timeouts each call the event tracker.
The extension sends workflow event data to ClearTok's tracking endpoint.
The code builds JSON with event_name, session_id, version, run_id, and event-specific fields before making a PUT request.
Dynamic analysis observed a PUT to this endpoint with a session_initialized payload containing session_id, version, and run_id; username and process metrics were not captured in traffic.
- Session identifiersess_9f3d2a6c18b04e7a
Lets ClearTok associate multiple workflow events from the same extension session.
- TikTok usernamecleartok_user_247
Identifies the TikTok account that the extension sees as logged in on the page.
- Event nameprocess_completed
Shows which workflow stage happened, such as start, completion, timeout, or error.
- TikTok page URLhttps://www.tiktok.com/@cleartok_user_247/repost
Shows which TikTok page was open when an element lookup timed out.
- Removal countsreposts_removed=24, processed_videos=31, skipped_videos=7
Shows how many reposts were found, processed, removed, skipped, or failed during your run.
- Timing and quota statetotal_duration_seconds=418, is_premium=false, remaining=76
Shows how long the run lasted and what quota or subscription state was attached at the start.
- Error and timeout detailsselector_key=video.repostButton, timeout_ms=15000, stop_reason=repost_tab_not_ready
Shows which selector failed or why the workflow stopped when the extension encounters a problem.
The popup builds event payloads and the API service sends them
Workflow start event
popup.jsasync startRemoval() { // 强制刷新配额 try { await this.quotaManager.getQuota(this.sessionAuth.sessionId, true); } catch (_) { } const isPlus = !!this.quotaManager.quotaInfo.is_premium; const remaining = Math.max(0, this.quotaManager.quotaInfo.remaining ?? this.quotaManager.getRemainingQuota()); if (!isPlus && remaining <= 0) { const base = this.quotaManager.quotaInfo.authenticated ? 'https://cleartok.io/premium/' : 'https://cleartok.io/login/'; this._openUrlWithParams(base); return; } // 上报 process_started const uid = this.quotaManager.accountInfo?.id ?? null; this.eventTracker.startRun({ is_premium: isPlus, remaining_at_start: remaining, }); this.eventTracker.track(EVENTS.PROCESS_START, { uid, is_premium: isPlus, remaining, }); await this.processLogger.startRemoval();}Login, timeout, error, and completion events
popup.jshandleLoginStatusUpdate(message) { if (message.isLoggedIn) { if (message.username && message.username !== this.sessionAuth.tikTokUsername) { this.sessionAuth.tikTokUsername = message.username; this.sessionAuth.saveSessionToStorage(); this.eventTracker.track(EVENTS.USER_LOGIN, { tiktok_username: message.username }); } this.sessionAuth.updateLoginStatus('loggedIn'); } else { if (this.sessionAuth.tikTokUsername !== null) { this.sessionAuth.tikTokUsername = null; this.eventTracker.track(EVENTS.USER_LOGOUT); } this.sessionAuth.updateLoginStatus('notLoggedIn'); ClearTokUtils.showNotification(ClearTokUtils.getText('notificationPleaseLogin') || '⚠️ Please log in to TikTok.com to continue', 'error'); }}handleUiWaitTimeout(message) { this.eventTracker.track(EVENTS.ELEMENT_TIMEOUT, { selector_key: message.selectorKey, timeout_ms: message.timeout, page_url: message.url, });}handleError(message) { this.processLogger.handleError(message.message, message.error); this.eventTracker.track(EVENTS.PROCESS_ERROR, { error_message: message.message, error_details: message.error ? String(message.error) : '', status_category: message.statusCategory || 'error', stop_reason: message.stopReason || 'unexpected_error', }); if (message.message && message.message.includes('No active TikTok tab found')) { this.processLogger.resetToInitialState(); }}handleComplete(message) { // UI 更新(纯展示) this.processLogger.handleCompletion(message); // 数据上报(统一在此处编排) const removedCount = message.removedCount || this.processLogger.removedVideos || 0; const duration = message.duration || 0; const totalDurationSeconds = this.sessionAuth.sessionStartTime ? Math.floor((Date.now() - this.sessionAuth.sessionStartTime) / 1000) : (duration ? Math.floor(duration.total / 1000) : 0); const stopReason = message.stopReason || 'completed'; const statusCategory = ClearTokExtension.workflowStatusCategory(stopReason, removedCount); this.eventTracker.track(EVENTS.PROCESS_COMPLETE, { status_category: statusCategory, stop_reason: stopReason, total_reposts_found: message.totalCount || this.processLogger.totalVideos || 0, reposts_removed: removedCount, processed_videos: message.processedVideos || this.processLogger.processedVideos || 0, skipped_videos: message.skippedVideos || 0, failed_videos: message.failedVideos || 0, batch_count: message.batchCount || 0, skip_reasons: message.skipReasons || {}, total_duration_seconds: totalDurationSeconds, }); if (removedCount > 0) { this.eventTracker.reportUsage(removedCount); this.quotaManager.updateUsage(removedCount); this.sessionAuth.updateStartButtonLabel(); }}Username extraction in the TikTok page
modules/workflow.jsasync runInitialChecks() { try { const profileLink = this.ui.findElement('loginStatus.profileLink'); if (!profileLink || !profileLink.getAttribute('href')?.includes('/@')) { await this.messageBus.broadcast('LOGIN_STATUS_UPDATE', { isLoggedIn: false }); return; } const href = profileLink.getAttribute('href'); const usernamePart = href.split('/@')[1]; const isLoggedIn = usernamePart && usernamePart.trim().length > 0; const hasAvatar = this.ui.findElement('loginStatus.avatarImage', profileLink) !== null; await this.messageBus.broadcast('LOGIN_STATUS_UPDATE', { isLoggedIn: isLoggedIn, username: isLoggedIn ? usernamePart.trim() : null, hasUserAvatar: hasAvatar, }); console.log('[ClearTok] Login status checked:', { isLoggedIn }); } catch (error) { console.error('[ClearTok] Error checking login status:', error); await this.messageBus.broadcast('LOGIN_STATUS_UPDATE', { isLoggedIn: false, error: error.message }); }}Event tracker and PUT serialization
popup/modules/event-tracker.js and api-service.jsasync track(eventName, data = {}) { if (!this.sessionId) { await ClearTokUtils.sleep(2000); if (!this.sessionId) { console.warn(`[EventTracker] 无 sessionId,跳过事件: ${eventName}`); return; } } if (ONCE_PER_PROCESS_EVENTS.has(eventName)) { const key = `${this.sessionId}:${eventName}`; if (this._sentEvents.has(key)) { console.log(`[EventTracker] 去重跳过: ${eventName}`); return; } this._sentEvents.add(key); } const payload = { event_name: eventName, run_id: this.runId, ...this.runContext, ...data, }; try { await this.api.updateSession(this.sessionId, payload); console.log(`[EventTracker] ✅ ${eventName}`, data); } catch (error) { console.warn(`[EventTracker] 上报失败: ${eventName}`, error); }}async updateSession(sessionId, data) { if (!sessionId) { console.warn('Session ID is empty, skipping event tracking.'); return { success: false, message: 'Session ID is required' }; } // 确保 event_name 存在,因为这是新模型的强制要求 if (!data.hasOwnProperty('event_name')) { console.error('API call is missing "event_name" property.', data); return { success: false, message: 'event_name property is required for the new API model.' }; } try { const response = await this.request('/session/track-event', { method: 'PUT', body: JSON.stringify({ session_id: sessionId, version: this.version, ...data }) }); return response; } catch (error) { console.error(`Failed to track event`, error); // 不抛出错误,避免因分析数据上报失败而影响扩展的核心功能 return { success: false, error: error.message }; }}- api.cleartok.io
Receives session and workflow tracking events for the ClearTok extension, including the /session/track-event endpoint.
Where it sends data
Destinations our analysis observed ClearTok contacting. Sending data somewhere is not a finding on its own - an extension that syncs your settings has to talk to its own server - but it is where your data can go, and who else it goes to.
- api.cleartok.io
ClearTok sends data to api.cleartok.io. No other extension we have analysed sends data here.