GuideFebruary 16, 20277 min read

Building ConfigSync Plugins: A Developer Guide

The complete guide to building ConfigSync plugins: the plugin interface, the detect/capture/restore lifecycle, manifest files, testing, and contributing to the ecosystem.

Why Build a Plugin

ConfigSync ships with 24 built-in modules that cover the most common developer tools: shells, editors, git, SSH, cloud CLIs, and package managers. But the ecosystem of developer tools is vast, and no built-in set can cover everything. Plugins let you extend ConfigSync to sync any tool's configuration.

Maybe you use a niche database client with a complex config file. Maybe your team has an internal CLI tool with settings that need to be consistent. Maybe you use a tool that just shipped a new config format and ConfigSync has not caught up yet. In each case, a plugin is the answer.

Plugins are Python modules that implement a simple three-method interface. If you can write Python, you can build a plugin.

The Plugin Interface

Every ConfigSync plugin extends the DevSyncPlugin base class and implements three methods: detect(), capture(), and restore().

The plugin interface
from devsync.plugin_system import DevSyncPlugin class MyToolPlugin(DevSyncPlugin): """Plugin for syncing MyTool configuration.""" def detect(self) -> bool: """Return True if MyTool is installed on this machine.""" # Check if the tool exists pass def capture(self, crypto_manager) -> dict: """Capture MyTool's current configuration state.""" # Read config files, return state dict pass def restore(self, state: dict, crypto_manager) -> None: """Restore MyTool's configuration from state.""" # Write config files from state dict pass

The detect() method is called during scanning to determine if this plugin is relevant on the current machine. If it returns False, the plugin is skipped. The capture() method reads the tool's config files and returns a dictionary representing the state. The restore() method takes that dictionary and writes the config files back.

Plugin Directory Structure

Plugins live in the configsync-plugins repository with a standard directory layout:

Plugin file structure
plugins/ └── mytool/ ├── plugin.py # Plugin implementation ├── manifest.yaml # Plugin metadata ├── README.md # Documentation └── tests/ ├── __init__.py └── test_plugin.py # Tests

The Manifest File

Every plugin includes a manifest.yaml that declares metadata about the plugin. This is used by ConfigSync to display information, categorize the plugin, and check compatibility.

manifest.yaml
name: mytool display_name: MyTool description: Sync MyTool configuration files version: 1.0.0 author: Your Name category: development # Options: ai_tool, editor, browser, # cloud, database, development, # security, system platforms: - macos - linux config_paths: - ~/.config/mytool/config.yaml - ~/.config/mytool/themes/ requires_encryption: - ~/.config/mytool/credentials.json
The category field determines where the plugin appears in ConfigSync's module list. Choose the category that best matches your tool. The requires_encryption field lists config files that contain secrets and should always be encrypted.

A Complete Example: Redis CLI Plugin

Here is a complete, realistic plugin for syncing the Redis CLI configuration:

plugins/redis/plugin.py
from pathlib import Path import shutil from devsync.plugin_system import DevSyncPlugin class RedisPlugin(DevSyncPlugin): """Sync redis-cli configuration and history.""" CONFIG_PATH = Path.home() / ".redisclirc" HISTORY_PATH = Path.home() / ".rediscli_history" def detect(self) -> bool: """Check if redis-cli is installed.""" return shutil.which("redis-cli") is not None def capture(self, crypto_manager) -> dict: """Capture redis-cli config and optionally history.""" state = {} if self.CONFIG_PATH.exists(): state["config"] = self.CONFIG_PATH.read_text() if self.HISTORY_PATH.exists(): # Only capture last 500 lines of history lines = self.HISTORY_PATH.read_text().splitlines() state["history"] = "\n".join(lines[-500:]) return state def restore(self, state: dict, crypto_manager) -> None: """Restore redis-cli config and history.""" if "config" in state: self.CONFIG_PATH.write_text(state["config"]) self.CONFIG_PATH.chmod(0o644) if "history" in state: self.HISTORY_PATH.write_text(state["history"]) self.HISTORY_PATH.chmod(0o600)

Handling Encrypted Files

If your plugin manages files that contain secrets, use the crypto_manager parameter to encrypt and decrypt them.

Using crypto_manager
def capture(self, crypto_manager) -> dict: state = {} # Regular config (not encrypted) if self.CONFIG_PATH.exists(): state["config"] = self.CONFIG_PATH.read_text() # Credentials (encrypted) if self.CREDS_PATH.exists(): raw = self.CREDS_PATH.read_bytes() state["credentials"] = crypto_manager.encrypt(raw) return state def restore(self, state: dict, crypto_manager) -> None: if "config" in state: self.CONFIG_PATH.write_text(state["config"]) if "credentials" in state: raw = crypto_manager.decrypt(state["credentials"]) self.CREDS_PATH.write_bytes(raw) self.CREDS_PATH.chmod(0o600) # Restrictive permissions

Writing Tests

Every plugin should include tests. ConfigSync uses pytest, and your tests should verify detect, capture, and restore behavior.

tests/test_plugin.py
import pytest from pathlib import Path from unittest.mock import MagicMock from plugins.redis.plugin import RedisPlugin def test_detect_with_redis_installed(monkeypatch): monkeypatch.setattr("shutil.which", lambda x: "/usr/local/bin/redis-cli") plugin = RedisPlugin() assert plugin.detect() is True def test_detect_without_redis(monkeypatch): monkeypatch.setattr("shutil.which", lambda x: None) plugin = RedisPlugin() assert plugin.detect() is False def test_capture_and_restore(tmp_path, monkeypatch): config_path = tmp_path / ".redisclirc" config_path.write_text("# Redis config\nset prompt \"> \"") monkeypatch.setattr(RedisPlugin, "CONFIG_PATH", config_path) monkeypatch.setattr(RedisPlugin, "HISTORY_PATH", tmp_path / ".history") plugin = RedisPlugin() crypto = MagicMock() state = plugin.capture(crypto) assert "config" in state config_path.unlink() plugin.restore(state, crypto) assert config_path.read_text() == "# Redis config\nset prompt \"> \"" # Run with: pytest plugins/redis/tests/

Contributing Your Plugin

Once your plugin is built and tested, contributing it to the ConfigSync ecosystem follows the standard open source workflow:

StepActionDetails
1Fork the repogithub.com/InventiveHQ/configsync-plugins
2Create plugin directoryplugins/<name>/ with standard layout
3Implement the interfacedetect(), capture(), restore()
4Add manifest.yamlMetadata, category, platforms
5Write testspytest in plugins/<name>/tests/
6Submit PRInclude README with usage examples

The ConfigSync community reviews plugin PRs for security (especially around credential handling), correctness (capture and restore are symmetric), and cross-platform compatibility. Plugins that pass review are included in the official collection and available to all ConfigSync users.

Building plugins is the fastest way to extend ConfigSync to cover your exact toolchain. The interface is intentionally simple: detect, capture, restore. If your tool stores configuration in files, you can sync it.

Ready to try ConfigSync?

Sync your entire dev environment across machines in minutes. Free forever for up to 3 devices.