Skills Permissions and Security Control

Skills can access the file system, call external APIs, and execute scripts. If abused, these capabilities can introduce security risks.

This article explains how to establish security boundaries at the design stage to prevent a Skill from performing operations beyond expectations.


Default Permission Scope of Skills

In Claude's execution environment, Skills' permissions are determined by the sandbox environment and are not unlimited.

Operation TypePermission StatusDescription
Read uploaded filesAllowedLimited to /mnt/user-data/uploads/
Write output filesAllowedLimited to /mnt/user-data/outputs/ and /home/claude/
Read system filesRestrictedRead-only mount, cannot modify system files
Access external networkRestrictedOnly whitelisted domains are allowed
Execute arbitrary system commandsRestrictedCannot use sudo, cannot modify system configuration
Access other users' dataProhibitedGuaranteed by sandbox isolation

Skills are a practice of the "least privilege" principle in design: a Skill can only access the resources it explicitly needs. If a Skill requires permissions beyond the above scope, the design approach should be reconsidered.


Clearly Define Permission Boundaries in SKILL.md

Clearly declare in the Skill documentation which resources it will access, so users can understand the Skill's scope of behavior before using it.

## 权限说明

本 Skill 会进行以下操作,请确认你已了解:

**文件访问**
- 读取:/mnt/user-data/uploads/ 下用户上传的文件
- 写入:/mnt/user-data/outputs/ 下的输出文件

**网络访问**
- 无(本 Skill 不访问任何外部网络)

**不会进行的操作**
- 不读取系统文件
- 不修改已上传的原始文件
- 不访问任何外部服务

Preventing Path Traversal Attacks

When a script accepts a user-provided file path, it must verify whether the path is within the allowed range, preventing users from../accessing directories they should not access.

Example

# File path: scripts/safe_path.py
import os

# Directories allowed for reading
ALLOWED_READ_DIRS = [
    "/mnt/user-data/uploads",
    "/mnt/skills/public",
]

# Directories allowed for writing
ALLOWED_WRITE_DIRS = [
    "/mnt/user-data/outputs",
    "/home/claude",
]

def is_safe_path(path: str, allowed_dirs: list) -> bool:
    """
Check whether the path is within the allowed directory scope

Prevent path traversal attacks like ../../../etc/passwd
    """

    # Resolve to absolute path (eliminate .. and symbolic links)
    real_path = os.path.realpath(os.path.abspath(path))

    for allowed in allowed_dirs:
        real_allowed = os.path.realpath(allowed)
        # Check whether real_path starts with the allowed directory
        if real_path.startswith(real_allowed + os.sep) or real_path == real_allowed:
            return True
    return False

def safe_read_path(user_input: str) -> str:
    """Validate the read path, raise an exception if invalid"""
    if not is_safe_path(user_input, ALLOWED_READ_DIRS):
        raise PermissionError(
            f"Access denied: {user_input}\n"
            f"Only the following directories are allowed for reading: {ALLOWED_READ_DIRS}"
        )
    return os.path.realpath(user_input)

def safe_write_path(user_input: str) -> str:
    """Validate the write path, raise an exception if invalid"""
    if not is_safe_path(user_input, ALLOWED_WRITE_DIRS):
        raise PermissionError(
            f"Write denied: {user_input}\n"
            f"Only the following directories are allowed for writing: {ALLOWED_WRITE_DIRS}"
        )
    return os.path.realpath(user_input)

# Usage example
if __name__ == "__main__":
    # Normal path: pass
    ok_path = safe_read_path("/mnt/user-data/uploads/example.csv")
    print(f"Passed: {ok_path}")

    # Traversal path: rejected
    try:
        bad_path = safe_read_path("/mnt/user-data/uploads/../../etc/passwd")
    except PermissionError as e:
        print(f"Blocked: {e}")
通过:/mnt/user-data/uploads/example.csv
已拦截:拒绝访问:/mnt/user-data/uploads/../../etc/passwd
只允许读取以下目录:['/mnt/user-data/uploads', '/mnt/skills/public']

Secure Storage of API Keys

Different key management methods vary significantly in security level.

MethodSecurityRecommendation Level
Hardcoded in scriptsExtremely low, will be committed to GitProhibited
Environment variablesMedium, process isolationRecommended (development stage)
.env file (added to .gitignore)Medium, local file storageRecommended (local use)
System key manager (such as Vault)High, centralized managementRecommended (production environment)

Example

# File path: scripts/config.py
# Safely read configuration from multiple sources, searching in descending order of priority

import os

def get_secret(key: str, required: bool = True) -> str:
    """
Read keys from the following sources in order of priority:
1. Environment variables (highest priority)
2. /home/claude/.skill_secrets file (local key file)
3. If required=True and not found, raise an exception
    """

    # 1. Environment variables
    value = os.environ.get(key)
    if value:
        return value

    # 2. Local key file (each line format: KEY=VALUE)
    secrets_file = "/home/claude/.skill_secrets"
    if os.path.exists(secrets_file):
        with open(secrets_file) as f:
            for line in f:
                line = line.strip()
                if line.startswith(f"{key}="):
                    return line[len(key)+1:]

    # 3. Not found
    if required:
        raise EnvironmentError(
            f"Missing required key: {key}\n"
            f"Please set the environment variable: export {key}='your key'"
        )
    return ""

Secure Filtering of Input Content

When a Skill passes user input to Shell commands, the input must be escaped to prevent command injection.

Example

# File path: scripts/safe_exec.py
import subprocess
import shlex

def safe_shell_exec(template: str, user_input: str) -> str:
    """
Safely embed user input into Shell commands

Wrong approach: os.system(f"process {user_input}") # Command injection risk!
Correct approach: use an argument list, let subprocess handle escaping
    """

    # Use a list instead of a string; subprocess will automatically handle escaping
    cmd = ["python", "scripts/process.py", user_input]
    result = subprocess.run(cmd, capture_output=True, text=True, timeout=30)
    return result.stdout

# If you must build a string command, use shlex.quote to escape user input
def safe_string_exec(user_filename: str) -> str:
    safe_name = shlex.quote(user_filename)   # Automatically add quotes and escape special characters
    cmd = f"wc -l {safe_name}"
    result = subprocess.run(cmd, shell=True, capture_output=True, text=True)
    return result.stdout

Never useos.system(f"cmd {user_input}")this way to execute commands. If user input contains; rm -rf /or similar content, it will lead to disastrous consequences. Always use subprocess's list argument form.

Other Extensions