Back to skill

Security audit

JobClaw

Security checks for vulnerabilities and agentic risk

Overview

JobClaw is a coherent job-search automation skill, but unsafe implementation choices could let remote job data or arbitrary URLs affect the user's machine.

Install only if you are comfortable with a scheduled local automation that searches job sites, writes CSV files, optionally sends job summaries through Telegram or OpenClaw, and archives records. Before use, fix the heredoc injection and URL-validation issues, avoid --break-system-packages, pin dependencies, store Telegram tokens in a secret store or locked-down file, and review cron/auto-archive behavior.

Vulnerability Patterns
  • Insecure DependenciesIntroduces malicious components through unsafe dependency sources
  • Insecure Skill Coding PracticesFinds exploitable flaws such as hardcoded secrets or command injection
  • Skill Instruction HijackingAlters the agent's session goals or safety constraints when the skill loads
  • Agent Memory PoisoningWrites attacker-controlled rules into memory that affect later sessions
  • Remote Payload Retrieval and ExecutionFetches external code whose behavior can change after review
Findings (4)

T09 · Insecure Skill Coding Practices

Error
Location
scripts/run_daily.sh:73
Finding

Remote Job Listing Data Is Interpolated into Executable Python Source

Content
View full analysis
/dev/null || echo "0") NONCODING_ADDED=$(python3 -c "import json; d=json.load(open('$SUMMARY_FILE')); print(d.get('noncoding',{}).get('added',0))" 2>/dev/null || echo "0") CODING_TOP=$(python3 -c "import json; d=json.load(open('$SUMMARY_FILE')); print(d.get('coding',{}).get('top','None'))" 2>/dev/null || echo "None") NONCODING_TOP=$(python3 -c "import json; d=json.load(open('$SUMMARY_FILE')); print(d.get('noncoding',{}).get('top','None'))" 2>/dev/null || echo "None") fi ``` ```bash python3 - <&1 | tee -a "$LOG_FILE" || true import sys, json, os sys.path.insert(0, '$SCRIPT_DIR') os.environ['JOBCLAW_DIR'] = '$JOBCLAW_DIR' from notify import send_summary with open('$JOBCLAW_DIR/config.json') as f: config = json.load(f) send_summary( config=config, total_jobs=$TOTAL_JOBS if '$TOTAL_JOBS' != '?' else 0, new_today=$NEW_TODAY, coding_added=$CODING_ADDED, noncoding_added=$NONCODING_ADDED, top_coding='$CODING_TOP', top_noncoding='$NONCODING_TOP', archived=${ARCHIVED_COUNT:-0}, ) PYEOF ``` ### Technical Analysis `CODING_TOP` and `NONCODING_TOP` are read from `search_summary.json`. These values contain company and job-title data obtained from external job listings. The values are subsequently expanded by the shell directly into Python source inside an unquoted heredoc. Shell quoting in the generated Python text does not safely encode these values. A listing containing a single quote, newline, comment character, or other Python syntax can terminate the intended string literal and add attacker-controlled Python expressions or statements. `JOBCLAW_DIR` a ...[truncated 1803 chars]
Remediation
View remediation

T09 · Insecure Skill Coding Practices

Error
Location
scripts/archiver.py:96
Finding

Unrestricted Job URL Validation Permits Server-Side Request Forgery

Content
View full analysis
bool | None: """ Returns: True — URL alive False — URL dead (404/410 or dead phrase in body) None — inconclusive (timeout, network error, LinkedIn) """ if not url or "linkedin.com" in url: return None # can't reliably check LinkedIn try: # HEAD check for status code result = subprocess.run( ["curl", "-s", "-o", "/dev/null", "-w", "%{http_code}", "--max-time", "8", "--location", url], capture_output=True, text=True, timeout=12 ) code = int(result.stdout.strip() or "0") if code in (404, 410): return False if 200 <= code < 400: # GET to check for dead phrases in body body_result = subprocess.run( ["curl", "-s", "--max-time", "8", "--location", url], capture_output=True, text=True, timeout=12 ) body = body_result.stdout.lower() if any(p in body for p in DEAD_PHRASES): return False return True return None # 5xx or other = inconclusive except Exception: return None ``` ```python # Rule 3: URL check (non-LinkedIn only) elif status in ("New", "Interested") and job_url and "linkedin.com" not in job_url: alive = check_url_alive(job_url) if alive is False: reason = "url_dead" ``` ### Technical Analysis Job URLs stored in the CSV are passed to `curl` without validation of their scheme, hostname, port, DNS resolution, or destination address. Redirect following is explicitly enabled with `--location`, and redirect destinations are not checked. A URL can originate from a remotely scraped listing or from the do ...[truncated 2091 chars]
Remediation
View remediation

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/setup.py:252
Finding

Telegram Bot Credential Is Stored in Plaintext with No Enforced Access Restrictions

Content
View full analysis
bool: """Send via Telegram Bot API directly.""" url = f"https://api.telegram.org/bot{bot_token}/sendMessage" data = urllib.parse.urlencode({ "chat_id": chat_id, "text": message, "parse_mode": "HTML", }).encode() try: req = urllib.request.Request(url, data=data, method="POST") with urllib.request.urlopen(req, timeout=30) as resp: result = json.loads(resp.read()) return result.get("ok", False) except Exception as e: print(f"[notify] Telegram send failed: {e}") return False ``` ```bash python3 - <&1 | tee -a "$LOG_FILE" || true import sys, json, os sys.path.insert(0, '$SCRIPT_DIR') os.environ['JOBCLAW_DIR'] = '$JOBCLAW_DIR' from notify import send_summary with open('$JOBCLAW_DIR/config.json') as f: config = json.load(f) ``` ### Technical Analysis The Telegram bot token is collected and written directly into `config.json`. The setup process does not set a restrictive process umask, create the file atomically with mode `0600`, or verify the resulting permissions. On ...[truncated 2179 chars]
Remediation
View remediation

T08 · Insecure Dependencies

Warning
Location
scripts/setup.py:108
Finding

Setup Installs Unpinned Dependencies into the Active or System Python Environment

Content
View full analysis
bool: print(f" Installing {package}...") result = subprocess.run( [sys.executable, "-m", "pip", "install", package, "--break-system-packages"], capture_output=True, text=True, ) if result.returncode == 0: print(f" ✅ {package} installed.") return True # Try without --break-system-packages result2 = subprocess.run( [sys.executable, "-m", "pip", "install", package], capture_output=True, text=True, ) if result2.returncode == 0: print(f" ✅ {package} installed.") return True print(f" ❌ Failed to install {package}. Install manually: pip install {package}") return False ``` ```python required_packages = ["jobspy", "openpyxl"] all_ok = True for pkg in required_packages: pkg_import = "jobspy" if pkg == "jobspy" else pkg if check_pip_package(pkg_import): print(f" ✅ {pkg}") else: print(f" ⚠️ {pkg} not found.") if not non_interactive: if prompt_yn(f"Install {pkg}?", default=True): ok = install_package(pkg if pkg != "jobspy" else "python-jobspy") if not ok: all_ok = False else: install_package(pkg if pkg != "jobspy" else "python-jobspy") ``` ### Technical Analysis The setup wizard installs `python-jobspy` and `openpyxl` without exact version constraints or integrity hashes. The effective code installed can therefore change over time without any change to the reviewed Skill package. The first installation attempt uses `--break-system-packages`, deliberately bypassing protections intended to prevent `pip` from modifying an externally managed Python environment. This can alter packages used by ...[truncated 1927 chars]
Remediation
View remediation
Vulnerability Patterns
  • Prompt InjectionInstruction Override, Hidden Instructions, Exfiltration Commands
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Excessive AgencyUnrestricted Tool Access, Autonomous Decision Making, Scope Creep
  • Trigger AbuseOverly Broad Trigger, Shadow Command Trigger, Keyword Baiting Trigger
  • Behavioral ASTexec() Call, eval() Call, Dynamic Import
Findings (24)

Tp4

High
Category
MCP Tool Poisoning
Confidence
95% confidence
Finding

The manifest promises a focused job-search skill, but the documented behavior includes unrelated or broader operational capabilities such as archive/restore flows and external URL checks. This mismatch undermines user and platform trust because operators may approve the skill for one purpose while it exercises different behaviors, which can mask risky actions and weaken security review.

Content

No source excerpt is available for this finding.

Undeclared Tool Scope

Medium
Category
MCP Least Privilege
Confidence
95% confidence
Finding

The skill documents use of shell, file writes, network access, and environment variables, but it declares no explicit tool scope or permissions boundary. That creates an overprivileged execution model where an agent may perform filesystem, network, and command execution actions without a clear least-privilege contract, increasing the chance of unintended or unsafe operations.

Content

No source excerpt is available for this finding.

Vague Triggers

Medium
Category
Not specified by scanner
Confidence
94% confidence
Finding

The trigger phrases are broad and overlap with common user requests about jobs, making accidental invocation more likely. In this skill, accidental activation is more dangerous because activation can lead to filesystem writes, shell execution, network access, and collection of personal/job-search preferences without a sharply bounded invocation phrase.

Content

No source excerpt is available for this finding.

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
93% confidence
Finding

The setup flow asks users for sensitive notification credentials such as Telegram bot tokens and chat IDs, then writes configuration to disk, but it provides no warning about storage, redaction, or handling. This creates a realistic secret-exposure risk: tokens may be stored in plaintext, echoed in chat history, logged, or reused to send unauthorized messages if compromised.

Content

No source excerpt is available for this finding.

subprocess module call

Medium
Category
Dangerous Code Execution
Confidence
70% confidence
Finding

subprocess module calls execute external commands. Without careful input validation, this enables command injection.

Content

Scanner excerpt · scripts/archiver.py (reported line 101)May include surrounding context.

python
try:
        # HEAD check for status code
        result = subprocess.run(
            ["curl", "-s", "-o", "/dev/null", "-w", "%{http_code}",
             "--max-time", "8", "--location", url],
            capture_output=True, text=True, timeout=12

subprocess module call

Medium
Category
Dangerous Code Execution
Confidence
70% confidence
Finding

subprocess module calls execute external commands. Without careful input validation, this enables command injection.

Content

Scanner excerpt · scripts/archiver.py (reported line 111)May include surrounding context.

python
return False
        if 200 <= code < 400:
            # GET to check for dead phrases in body
            body_result = subprocess.run(
                ["curl", "-s", "--max-time", "8", "--location", url],
                capture_output=True, text=True, timeout=12
            )

subprocess module call

Medium
Category
Dangerous Code Execution
Confidence
70% confidence
Finding

subprocess module calls execute external commands. Without careful input validation, this enables command injection.

Content

Scanner excerpt · scripts/notify.py (reported line 130)May include surrounding context.

python
if not openclaw_bin:
        # Try via PATH
        result = subprocess.run(["which", "openclaw"], capture_output=True, text=True)
        if result.returncode == 0:
            openclaw_bin = result.stdout.strip()

subprocess module call

Medium
Category
Dangerous Code Execution
Confidence
70% confidence
Finding

subprocess module calls execute external commands. Without careful input validation, this enables command injection.

Content

Scanner excerpt · scripts/notify.py (reported line 142)May include surrounding context.

python
cmd += ["--account", account]

    try:
        result = subprocess.run(cmd, capture_output=True, text=True, timeout=30)
        return result.returncode == 0
    except Exception as e:
        print(f"[notify] OpenClaw send failed: {e}")

External Transmission

Medium
Category
Data Exfiltration
Confidence
60% confidence
Finding

Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

Content

Scanner excerpt · scripts/notify.py (reported line 151)May include surrounding context.

python
def _send_telegram(message: str, bot_token: str, chat_id: str) -> bool:
    """Send via Telegram Bot API directly."""
    url = f"https://api.telegram.org/bot{bot_token}/sendMessage"
    data = urllib.parse.urlencode({
        "chat_id": chat_id,
        "text": message,

Description-Behavior Mismatch

Medium
Category
Not specified by scanner
Confidence
91% confidence
Finding

The script sends notifications by loading user config and invoking send_summary, but notification behavior is not described in the skill metadata. Undisclosed outbound messaging can surprise users, leak job-search activity through configured channels, and expand the skill's effective privileges beyond what a user reasonably expects from a local CSV/job search tool.

Content

No source excerpt is available for this finding.

Description-Behavior Mismatch

Medium
Category
Not specified by scanner
Confidence
96% confidence
Finding

The daily runner performs an automatic archival step using archiver.py --commit, which changes local job-tracker data, but this behavior is not disclosed in the stated skill description. Undocumented state-changing behavior is dangerous because users may schedule this automation expecting search-only functionality and silently lose visibility of jobs that are moved or hidden by archival.

Content

No source excerpt is available for this finding.

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
97% confidence
Finding

The script executes archiver.py --commit automatically in the daily pipeline, which is an explicit data-modifying operation, without any user warning at runtime or prior consent in the described behavior. In a scheduled job-search context, this is risky because it can alter the user's tracker records unattended, making recovery harder and potentially causing missed opportunities if active jobs are archived incorrectly.

Content

No source excerpt is available for this finding.

Dynamic import via __import__()

Medium
Category
Dangerous Code Execution
Confidence
75% confidence
Finding

Dynamic import() can load arbitrary modules at runtime, bypassing static analysis and potentially importing malicious code.

Content

Scanner excerpt · scripts/setup.py (reported line 106)May include surrounding context.

python
def check_pip_package(package: str) -> bool:
    try:
        __import__(package.replace("-", "_"))
        return True
    except ImportError:
        return False

Context-Inappropriate Capability

Medium
Category
Not specified by scanner
Confidence
95% confidence
Finding

Automatically installing Python packages during setup expands the skill's capabilities from conversational onboarding to code and dependency modification on the host. That creates supply-chain and environment integrity risk, especially because installation can execute package-provided code and alter system packages.

Content

No source excerpt is available for this finding.

subprocess module call

Medium
Category
Dangerous Code Execution
Confidence
93% confidence
Finding

The setup script invokes pip to install packages during configuration, which is an external code execution path because package installation runs untrusted package metadata and installed code in the user's environment. Although the package name is currently chosen from a fixed internal list, using '--break-system-packages' increases the risk of damaging the host Python environment or introducing unintended code through dependency resolution.

Content

Scanner excerpt · scripts/setup.py (reported line 114)May include surrounding context.

python
def install_package(package: str) -> bool:
    print(f"  Installing {package}...")
    result = subprocess.run(
        [sys.executable, "-m", "pip", "install", package, "--break-system-packages"],
        capture_output=True, text=True,
    )

subprocess module call

Medium
Category
Dangerous Code Execution
Confidence
91% confidence
Finding

This fallback pip invocation performs package installation from the local Python interpreter without sandboxing, which still exposes the user to execution of third-party package install hooks and dependency supply-chain risk. In the context of a setup wizard, this exceeds simple configuration and can modify the system state unexpectedly.

Content

Scanner excerpt · scripts/setup.py (reported line 122)May include surrounding context.

python
print(f"  ✅ {package} installed.")
        return True
    # Try without --break-system-packages
    result2 = subprocess.run(
        [sys.executable, "-m", "pip", "install", package],
        capture_output=True, text=True,
    )

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
89% confidence
Finding

Sensitive Telegram and notification credentials are collected and written to 'config.json' on disk without an explicit warning, consent around local secret storage, or file-permission hardening. If the workstation is shared, backed up insecurely, or the config directory is exposed, these credentials could be recovered and abused for unauthorized messaging or account access.

Content

No source excerpt is available for this finding.

Description-Behavior Mismatch

Low
Category
Not specified by scanner
Confidence
90% confidence
Finding

The description says results are saved locally, but the workflow also transmits job data through Telegram/OpenClaw notifications. This is a data-flow transparency issue: users may reasonably expect local-only processing, yet their job search activity and possibly sensitive profile-derived content can be sent to third-party messaging systems.

Content

No source excerpt is available for this finding.

Context-Inappropriate Capability

Low
Category
Not specified by scanner
Confidence
76% confidence
Finding

The stated purpose is automated job search, scoring, and local CSV tracking. Collecting bot tokens/chat IDs and sending notifications introduces messaging/integration capability beyond that narrowly stated scope, even though it may be useful operationally.

Content

No source excerpt is available for this finding.

Missing User Warnings

Low
Category
Not specified by scanner
Confidence
79% confidence
Finding

The markdown states that python3 scripts/notify.py --test sends a sample message, which implies data is transmitted to an external notification channel. However, the documentation does not clearly warn users that this operation may contact a third-party service such as Telegram or OpenClaw, which is relevant to privacy and system behavior.

Content

No source excerpt is available for this finding.

Missing User Warnings

Low
Category
Not specified by scanner
Confidence
76% confidence
Finding

The config.json example includes telegram_bot_token and account identifiers, which are sensitive values. The markdown documents these fields but does not warn users to keep them secret or avoid committing them to source control, which is a relevant safety disclosure for a skill handling credentials.

Content

No source excerpt is available for this finding.

Missing User Warnings

Low
Category
Not specified by scanner
Confidence
76% confidence
Finding

The code locates and executes an external CLI through subprocess.run, which is a safety-relevant operation for code files. Although the function has an internal docstring, there is no confirmation prompt or stronger user-facing warning near execution, and the test CLI help does not mention invoking external tooling.

Content

No source excerpt is available for this finding.

Missing User Warnings

Low
Category
Not specified by scanner
Confidence
80% confidence
Finding

This function sends notification content and chat identifiers over the network to the Telegram Bot API, but there is no confirmation prompt and no user-facing disclosure near the operation beyond a terse internal docstring. For code-file warning checks, network calls that transmit user or system data should have some visible disclosure unless clearly covered elsewhere.

Content

No source excerpt is available for this finding.

Natural-Language Policy Violations

Low
Category
Not specified by scanner
Confidence
82% confidence
Finding

The defaults force London/UK and Europe/London settings, and non-interactive setup applies them automatically. This is a natural-language locale policy concern because the skill imposes a specific regional context without offering explicit user choice in that mode.

Content

No source excerpt is available for this finding.

Static analysis

No suspicious patterns detected.