Skills Permissions and Security Controls |
\\n\\n\\n\\n
Skills can access the file system, call external APIs, and execute scripts. If these capabilities are misused, they can pose security risks.
\\n\\nThis article explains how to establish security boundaries during the design phase to prevent Skills from performing actions beyond their intended scope.
\\n\\n\\n\\n
Default Permission Scope of Skills
\\n\\nIn Claudeβs execution environment, Skillsβ permissions are determined by the sandbox environment and are not unlimited.
\\n\\n| Operation Type | \\nPermission Status | \\nDescription | \\n
|---|---|---|
| Read uploaded files | \\nAllowed | \\nOnly within /mnt/user-data/uploads/ | \\n
| Write output files | \\nAllowed | \\nOnly within /mnt/user-data/outputs/ and /home/claude/ | \\n
| Read system files | \\nRestricted | \\nRead-only mount; system files cannot be modified | \\n
| Access external network | \\nRestricted | \\nOnly whitelisted domains are allowed | \\n
| Execute arbitrary system commands | \\nRestricted | \\nsudo is disallowed; system configuration cannot be modified | \\n
| Access other usersβ data | \\nProhibited | \\nGuaranteed by sandbox isolation | \\n
\\n\\n\\nSkills are designed following the principle of least privilege: a Skill can only access resources it explicitly requires. If a Skill needs permissions beyond the above scope, the design should be reconsidered.
\\n
\\n\\n
Defining Permission Boundaries in SKILL.md
\\n\\nClearly declare which resources the Skill will access in its documentation, so users understand its behavior scope before using it.
\\n\\nPermission Statement
\\n\\nThe Skill will perform the following operations. Please confirm that you understand them:
\\n\\n- \\n
- File Access\\n
- \\n
- Read: user-uploaded files under
/mnt/user-data/uploads/\\n - Write: output files under
/mnt/user-data/outputs/\\n
\\n - Read: user-uploaded files under
- Network Access\\n
- \\n
- None (this Skill does not access any external networks) \\n
\\n - Operations Not Performed\\n
- \\n
- Does not read system files \\n
- Does not modify original uploaded files \\n
- Does not access any external services \\n
\\n
\\n\\n
Preventing Path Traversal Attacks
\\n\\nWhen scripts accept file paths provided by users, they must validate whether the path falls within allowed directories to prevent users from using ../ to access unauthorized directories.
Example
\\n\\n# File path: scripts/safe_path.py\\n\\nimport os\\n\\n# Allowed read directories\\n\\n ALLOWED_READ_DIRS =[\\n"/mnt/user-data/uploads",\\n"/mnt/skills/public",\\n]\\n\\n# Allowed write directory\\n\\n ALLOWED_WRITE_DIRS =[\\n"/mnt/user-data/outputs",\\n"/home/claude",\\n]\\n\\ndef is_safe_path(path: str, allowed_dirs: list) ->bool:\\n\\n"""\\n Check if path is within allowed directory scope\\nPrevent ../../../etc/passwd type of path traversal attack\\n\\n """\\n\\n# Resolve to absolute path (eliminate .. and symbolic links)\\n\\n real_path =os.path.realpath(os.path.abspath(path))\\n\\nfor allowed in allowed_dirs:\\n\\n real_allowed =os.path.realpath(allowed)\\n\\n# Check if real_path starts with the allowed directory\\n\\nif real_path.startswith(real_allowed + os.sep)or real_path == real_allowed:\\n\\nreturn True\\n\\nreturn False\\n\\ndef safe_read_path(user_input: str) ->str:\\n\\n"""Validate read path, throw exception if invalid"""\\n\\nif not is_safe_path(user_input, ALLOWED_READ_DIRS):\\n\\nraise PermissionError(\\n\\n f"Access denied:{user_input}n"\\n\\n f"Only allow reading from the following directories:{ALLOWED_READ_DIRS}"\\n\\n)\\n\\nreturn os.path.realpath(user_input)\\n\\ndef safe_write_path(user_input: str) ->str:\\n\\n"""Validate write path, throw exception if invalid"""\\n\\nif not is_safe_path(user_input, ALLOWED_WRITE_DIRS):\\n\\nraise PermissionError(\\n\\n f"Deny write:{user_input}n"\\n\\n f"Only allow writing to the following directories:{ALLOWED_WRITE_DIRS}"\\n\\n)\\n\\nreturn os.path.realpath(user_input)\\n\\n# Usage example\\n\\nif __name__ =="__main__":\\n\\n# Normal path: Pass\\n\\n ok_path = safe_read_path("/mnt/user-data/uploads/tutorial.csv")\\n\\nprint(f"Pass:{ok_path}")\\n\\n# Traversal path: Denied\\n\\ntry:\\n\\n bad_path = safe_read_path("/mnt/user-data/uploads/../../etc/passwd")\\n\\nexcept PermissionError as e:\\n\\nprint(f"Intercepted:{e}")\\n\\nPass: /mnt/user-data/uploads/tutorial.csv Intercepted:Access denied:/mnt/user-data/uploads/../../etc/passwd Only allow reading from the following directories:['/mnt/user-data/uploads', '/mnt/skills/public']\\n\\n\\n\\n\\n
Secure Storage of API Keys
\\n\\nDifferent key management methods have significantly different security levels.
\\n\\n| Method | \\nSecurity Level | \\nRecommendation | \\n
|---|---|---|
| Hardcoded in script | \\nVery low; exposed in Git commits | \\nProhibited | \\n
| Environment variables | \\nModerate; process isolation | \\nRecommended (development phase) | \\n
.env file (added to .gitignore) | \\n Moderate; file stored locally | \\nRecommended (local use) | \\n
| System key manager (e.g., Vault) | \\nHigh; centralized management | \\nRecommended (production environment) | \\n
Example
\\n\\n# File path: scripts/config.py\\n\\n# Safely read configuration from multiple sources, searching in descending order of priority\\n\\nimport os\\n\\ndef get_secret(key: str, required: bool=True) ->str:\\n\\n"""\\n\\n Read key from the following sources in order of priority:\\n\\n 1. Environment variables (highest priority)\\n\\n 2. /home/claude/.skill_secrets File (local key file)\\n\\n 3. If required=True and not found, throw an exception\\n\\n """\\n\\n# 1. Environment variables\\n\\n value =os.environ.get(key)\\n\\nif value:\\n\\nreturn value\\n\\n# 2. Local key file (format per line: KEY=VALUEοΌ\\n\\n secrets_file ="/home/claude/.skill_secrets"\\n\\nif os.path.exists(secrets_file):\\n\\nwith open(secrets_file)as f:\\n\\nfor line in f:\\n\\n line = line.strip()\\n\\nif line.startswith(f"{key}="):\\n\\nreturn line[len(key)+1:]\\n\\n# 3. Not found\\n\\nif required:\\n\\nraise EnvironmentError(\\n\\n f"Missing required key:{key}n"\\n\\n f"Please set Environment variables: export {key}='Your key'"\\n\\n)\\n\\nreturn""\\n\\n\\n\\n\\n
Secure Filtering of Input Content
\\n\\nWhen a Skill passes user input to shell commands, the input must be escaped to prevent command injection.
\\n\\nExample
\\n\\n# File path: scripts/safe_exec.py\\n\\nimport subprocess\\n\\nimport shlex\\n\\ndef safe_shell_exec(template: str, user_input: str) ->str:\\n\\n"""\\n\\n Safely embed user input into Shell commands\\nWrong approach: os.system(f"process {user_input}") # Command injection risk!\\n\\n Correct approach: Use an argument list, let subprocess handle escaping\\n\\n """\\n\\n# Use list instead of string, subprocess will handle escaping automatically\\n\\ncmd=["python","scripts/process.py", user_input]\\n\\n result =subprocess.run(cmd, capture_output=True, text=True, timeout=30)\\n\\nreturn result.stdout\\n\\n# If you must build string commands, use shlex.quote Escape user input\\n\\ndef safe_string_exec(user_filename: str) ->str:\\n\\n safe_name =shlex.quote(user_filename)# Automatically add quotes and escape special characters\\n\\ncmd= f"wc -l {safe_name}"\\n\\n result =subprocess.run(cmd, shell=True, capture_output=True, text=True)\\n\\nreturn result.stdout\\n\\n> Never use `os.system(f"cmd {user_input}")` Execute commands this way. If user input contains `; rm -rf /` and similar content can lead to catastrophic consequences. Always use the list argument form of subprocess.\\n
YouTip