VSCode — Python Manipulation Guide

SkillFiles & storage

How to programmatically configure, modify, and verify VSCode settings, keybindings, extensions, and workspace files using Python. For setup-gen and reward-gen agents.

Available today. Use it from your connected AI after setup.

Connect ahel once, and every AI you use reads what you have installed.

Then ask your AI: use the VSCode — Python Manipulation Guide skill

What this skill tells your AI

The instructions your AI receives, as published by xlang-ai/cua-gym in .claude/skills/vs-code/SKILL.md and read by ahel’s review.

This skill teaches setup-gen (create/modify configs) and reward-gen (read/verify configs) how to work with VSCode configuration files using pure Python code.

  • Libraries: json, os, subprocess, shutil
  • Config root: ~/.config/Code/User/ (Linux)

0. GUI Startup on VM (for setup-gen)

After preparing workspace/settings, setup-gen should open VSCode with the target folder or file for the GUI agent.

CRITICAL VM LIMIT: GUI launches must set DISPLAY=:0.

import os
import shlex
import subprocess
import time

def launch_gui(command: str, delay_sec: float = 1.0):
    env = os.environ.copy()
    env["DISPLAY"] = ":0"
    subprocess.Popen(
        shlex.split(command),
        stdout=subprocess.DEVNULL,
        stderr=subprocess.DEVNULL,
        env=env,
    )
    time.sleep(delay_sec)

# Open workspace/folder
launch_gui('code "/home/user/workspace"', delay_sec=2.0)

# Or open a specific file
# launch_gui('code "/home/user/workspace/main.py"', delay_sec=2.0)

Guidelines:

  • Use non-blocking launch (Popen) and short delays.
  • For multi-app tasks, coordinate launch order explicitly.

1. Creating & Writing Files (setup-gen)

Key File Paths

import os, json, shutil, subprocess

HOME = os.path.expanduser("~")
VSCODE_USER = os.path.join(HOME, ".config", "Code", "User")
SETTINGS_PATH = os.path.join(VSCODE_USER, "settings.json")
KEYBINDINGS_PATH = os.path.join(VSCODE_USER, "keybindings.json")
SNIPPETS_DIR = os.path.join(VSCODE_USER, "snippets")

Settings (settings.json)

# Read existing settings (or start empty)
def load_settings():
    try:
        with open(SETTINGS_PATH, "r") as f:
            return json.load(f)
    except (FileNotFoundError, json.JSONDecodeError):
        return {}

# Write settings — merges with existing
def update_settings(updates: dict):
    settings = load_settings()
    settings.update(updates)
    os.makedirs(os.path.dirname(SETTINGS_PATH), exist_ok=True)
    with open(SETTINGS_PATH, "w") as f:
        json.dump(settings, f, indent=4)

# Examples
update_settings({
    "editor.fontSize": 16,
    "editor.wordWrap": "on",
    "workbench.colorTheme": "Visual Studio Dark",
    "debug.focusEditorOnBreak": False,
    "python.analysis.diagnosticSeverityOverrides": {
        "reportMissingImports": "none"
    }
})

Nested Settings

def update_nested_setting(key_path: list, value):
    """Set a deeply nested key like ["python", "analysis", "typeCheckingMode"]."""
    settings = load_settings()
    d = settings
    for key in key_path[:-1]:
        d = d.setdefault(key, {})
    d[key_path[-1]] = value
    with open(SETTINGS_PATH, "w") as f:
        json.dump(settings, f, indent=4)

Keybindings (keybindings.json)

def load_keybindings():
    """Load keybindings, handling the optional comment prefix line."""
    try:
        with open(KEYBINDINGS_PATH, "r") as f:
            content = f.read()
        try:
            return json.loads(content)
        except json.JSONDecodeError:
            # Skip first line (comment like "// Place your key bindings...")
            lines = content.split("\n", 1)
            if len(lines) > 1:
                return json.loads(lines[1])
            return []
    except FileNotFoundError:
        return []

def add_keybinding(key: str, command: str, when: str = None):
    bindings = load_keybindings()
    entry = {"key": key, "command": command}
    if when:
        entry["when"] = when
    bindings.append(entry)
    os.makedirs(os.path.dirname(KEYBINDINGS_PATH), exist_ok=True)
    with open(KEYBINDINGS_PATH, "w") as f:
        json.dump(bindings, f, indent=4)

# Examples
add_keybinding("ctrl+shift+t", "workbench.action.terminal.toggleTerminal")
add_keybinding("ctrl+k ctrl+c", "editor.action.addCommentLine", when="editorTextFocus")

Workspace Files (.code-workspace)

def create_workspace(workspace_path: str, folders: list, settings: dict = None):
    """Create a .code-workspace file."""
    workspace = {
        "folders": [{"path": p} for p in folders],
    }
    if settings:
        workspace["settings"] = settings
    with open(workspace_path, "w") as f:
        json.dump(workspace, f, indent=4)

# Example
create_workspace(
    "/home/user/project.code-workspace",
    ["/home/user/src", "/home/user/data1", "/home/user/data2"],
    settings={"editor.tabSize": 2}
)

Extension Management

# Install extension via CLI
def install_extension(extension_id: str):
    subprocess.run(["code", "--install-extension", extension_id], check=True)

# Uninstall extension
def uninstall_extension(extension_id: str):
    subprocess.run(["code", "--uninstall-extension", extension_id], check=True)

# List installed extensions
def list_extensions() -> str:
    result = subprocess.run(["code", "--list-extensions"], capture_output=True, text=True)
    return result.stdout

# Examples
install_extension("ms-python.python")
install_extension("esbenp.prettier-vscode")

Text File Creation/Modification

# Create a file for VSCode to open
def create_text_file(path: str, content: str):
    os.makedirs(os.path.dirname(path), exist_ok=True)
    with open(path, "w") as f:
        f.write(content)

# Find and replace in a file
def find_replace_in_file(path: str, old: str, new: str):
    with open(path, "r") as f:
        content = f.read()
    content = content.replace(old, new)
    with open(path, "w") as f:
        f.write(content)

# Golden file: copy then modify
shutil.copy("/home/user/original.py", "/home/user/golden.py")
find_replace_in_file("/home/user/golden.py", "text", "test")

Launching VSCode

# Open VSCode with a file
subprocess.Popen(["code", "/home/user/document.txt"])

# Open VSCode with a folder
subprocess.Popen(["code", "/home/user/project/"])

# Open VSCode with a workspace
subprocess.Popen(["code", "/home/user/project.code-workspace"])

Snippet Files

def create_snippet(language: str, name: str, prefix: str, body: list, description: str = ""):
    snippet_path = os.path.join(SNIPPETS_DIR, f"{language}.json")
    os.makedirs(SNIPPETS_DIR, exist_ok=True)
    try:
        with open(snippet_path, "r") as f:
            snippets = json.load(f)
    except (FileNotFoundError, json.JSONDecodeError):
        snippets = {}
    snippets[name] = {"prefix": prefix, "body": body, "description": description}
    with open(snippet_path, "w") as f:
        json.dump(snippets, f, indent=4)

2. Reading & Verifying Files (reward-gen)

Verifying Settings (Subset Check)

def verify_settings_subset(expected: dict) -> bool:
    """Check that all expected key-value pairs exist in settings.json."""
    settings = load_settings()
    return _is_subset(expected, settings)

def _is_subset(expected, actual) -> bool:
    if isinstance(expected, dict):
        if not isinstance(actual, dict):
            return False
        return all(k in actual and _is_subset(v, actual[k]) for k, v in expected.items())
    if isinstance(expected, list):
        return expected == actual
    return expected == actual

# Example: check theme was changed
assert verify_settings_subset({"workbench.colorTheme": "Visual Studio Dark"})
# Nested check
assert verify_settings_subset({
    "python.analysis.diagnosticSeverityOverrides": {"reportMissingImports": "none"}
})

Verifying Keybindings

def verify_keybinding(expected_entry: dict) -> bool:
    """Check that expected keybinding exists in keybindings.json."""
    bindings = load_keybindings()
    return expected_entry in bindings

# Example
assert verify_keybinding({"key": "ctrl+shift+t", "command": "workbench.action.terminal.toggleTerminal"})

Verifying Config with Containment

def verify_config_containment(file_path: str, expected_json_str: str) -> bool:
    """Check JSON file contains expected structure (subset match)."""
    try:
        with open(file_path, "r") as f:
            actual = json.load(f)
        expected = json.loads(expected_json_str)
        return _is_subset(expected, actual)
    except (FileNotFoundError, json.JSONDecodeError):
        return False

Verifying Text Files

import re

def verify_text_file(actual_path: str, expected_path: str,
                     ignore_blanks=False, ignore_case=False) -> bool:
    with open(actual_path) as f:
        actual = f.read()
    with open(expected_path) as f:
        expected = f.read()
    if ignore_blanks:
        actual = re.sub(r'[\t\n]', ' ', actual).strip()
        actual = re.sub(r'\s+', ' ', actual)
        expected = re.sub(r'[\t\n]', ' ', expected).strip()
        expected = re.sub(r'\s+', ' ', expected)
    if ignore_case:
        actual = actual.lower()
        expected = expected.lower()
    return actual == expected

Verifying Extensions

def verify_extension_installed(extension_id: str) -> bool:
    result = subprocess.run(["code", "--list-extensions"], capture_output=True, text=True)
    return extension_id in result.stdout

def verify_extension_not_installed(extension_id: str) -> bool:
    return not verify_extension_installed(extension_id)

Verifying Workspace Files

def verify_workspace_folders(workspace_path: str, expected_folders: list) -> bool:
    with open(workspace_path, "r") as f:
        ws = json.load(f)
    actual_paths = [folder["path"] for folder in ws.get("folders", [])]
    return set(expected_folders) == set(actual_paths)

Verifying Python Files by Test Suite

import importlib.util, sys, uuid

def run_test_on_file(test_file: str, test_func_name: str = "test") -> float:
    """Load a test file and run its test function. Returns 0.0-1.0."""
    original_cwd = os.getcwd()
    original_path = sys.path.copy()
    module_name = f"test_{uuid.uuid4().hex[:8]}"
    try:
        test_dir = os.path.dirname(os.path.abspath(test_file))
        os.chdir(test_dir)
        if test_dir not in sys.path:
            sys.path.insert(0, test_dir)
        spec = importlib.util.spec_from_file_location(module_name, test_file)
        module = importlib.util.module_from_spec(spec)
        sys.modules[module_name] = module
        spec.loader.exec_module(module)
        result = getattr(module, test_func_name)()
        if isinstance(result, bool):
            return 1.0 if result else 0.0
        return max(0.0, min(1.0, float(result)))
    except Exception:
        return 0.0
    finally:
        if module_name in sys.modules:
            del sys.modules[module_name]
        os.chdir(original_cwd)
        sys.path[:] = original_path

Verifying ZIP/Archive Files

import zipfile

def verify_zip_files(actual_zip: str, expected_zip: str) -> bool:
    with zipfile.ZipFile(actual_zip) as z1, zipfile.ZipFile(expected_zip) as z2:
        if set(z1.namelist()) != set(z2.namelist()):
            return False
        return all(z1.read(name) == z2.read(name) for name in z1.namelist())

3. Bitter Lessons

  1. keybindings.json may have a comment prefix line. VSCode writes // Place your key bindings in this file... as the first line. json.load() fails. Always try direct load first, then skip the first line as fallback.

  2. settings.json uses update not replace. Always load existing settings and merge. Overwriting the entire file destroys user settings that other setup steps depend on.

  3. _is_subset is the correct comparison for settings. The evaluator checks that expected keys exist with correct values, NOT that settings.json is an exact match. Extra keys are fine.

  4. Workspace file is JSON, not settings.json. .code-workspace files have a different schema (folders, settings, extensions). Don't confuse with ~/.config/Code/User/settings.json.

  5. Extension IDs are case-insensitive but must be exact. ms-python.python works; Python alone does not. Use code --list-extensions to get canonical IDs.

  6. code CLI may not be in PATH. In headless/VM environments, VSCode may need to be launched via full path (/usr/bin/code) or the code symlink may not exist. Check with which code first.

  7. Text file comparison: ignore_blanks collapses tabs and newlines to spaces. It replaces [\t\n] with space, then collapses multiple spaces. This means paragraph structure is lost. Use ignore_blanks=False when line structure matters.

  8. JSON settings with // comments are NOT valid JSON. VSCode's settings.json supports JSONC (JSON with Comments). Python's json module cannot parse comments. Use regex to strip them: re.sub(r'//.*$', '', content, flags=re.MULTILINE) before parsing.

  9. compare_config defaults to containment mode. The evaluator's containment_ok=True (default) means the expected JSON only needs to be a subset of the actual. Set containment_ok=False for strict equality.

  10. Python test suite runs in the test file's directory. The evaluator cds to the test file's parent directory before running. Relative paths in the test file resolve from there.

  11. Extension installation is async. code --install-extension returns before the extension is fully activated. If subsequent steps depend on the extension, add a delay or restart VSCode.

  12. Workspace folders use exact paths. {"path": "/home/user/data1"} is NOT equal to {"path": "~/data1"}. Always use fully expanded absolute paths when comparing.

Signals

GitHub stars
197
Forks
18
Last commit
Aug 2026
Advanced
Catalog kind
skill
Gateway key
vs-code
Source
github.com/xlang-ai/cua-gym