vs-code
Apps & AutomationHow to programmatically configure, modify, and verify VSCode settings, keybindings, extensions, and workspace files using Python. For setup-gen and reward-gen agents.
How to use this skill
Bring this guide into your coding agent with a prompt tailored to the tool you use.
- Open your project in Codex.
- Copy and paste the prompt below.
- Review files and risks before approving any action.
Inspect the Agent Skill at https://github.com/xlang-ai/CUA-Gym/blob/HEAD/.claude/skills/vs-code/SKILL.md and all supporting files. Report provenance, permissions, scripts, dependencies, and security risks. Propose an exact installation into .agents/skills/vs-code, but wait for my approval before writing files or running scripts.
Copying this prompt does not install or run the skill. Review third-party files before use.
VSCode — Python Manipulation Guide
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
-
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. -
settings.json uses
updatenotreplace. Always load existing settings and merge. Overwriting the entire file destroys user settings that other setup steps depend on. -
_is_subsetis 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. -
Workspace file is JSON, not settings.json.
.code-workspacefiles have a different schema (folders,settings,extensions). Don't confuse with~/.config/Code/User/settings.json. -
Extension IDs are case-insensitive but must be exact.
ms-python.pythonworks;Pythonalone does not. Usecode --list-extensionsto get canonical IDs. -
codeCLI may not be in PATH. In headless/VM environments, VSCode may need to be launched via full path (/usr/bin/code) or thecodesymlink may not exist. Check withwhich codefirst. -
Text file comparison:
ignore_blankscollapses tabs and newlines to spaces. It replaces[\t\n]with space, then collapses multiple spaces. This means paragraph structure is lost. Useignore_blanks=Falsewhen line structure matters. -
JSON settings with
//comments are NOT valid JSON. VSCode'ssettings.jsonsupports JSONC (JSON with Comments). Python'sjsonmodule cannot parse comments. Use regex to strip them:re.sub(r'//.*$', '', content, flags=re.MULTILINE)before parsing. -
compare_configdefaults to containment mode. The evaluator'scontainment_ok=True(default) means the expected JSON only needs to be a subset of the actual. Setcontainment_ok=Falsefor strict equality. -
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. -
Extension installation is async.
code --install-extensionreturns before the extension is fully activated. If subsequent steps depend on the extension, add a delay or restart VSCode. -
Workspace folders use exact paths.
{"path": "/home/user/data1"}is NOT equal to{"path": "~/data1"}. Always use fully expanded absolute paths when comparing.