Building a custom Claude code plugin for sublime text

25 min read

Integrating Claude Code with Sublime Text

Given how much I love utilizing WatchExec as part of my development workflow. I wanted to find a way to integrate Claude code in a similar way. the goal is to create a watcher style plugin that will provide insights on the work I am doing in real time. This plug in will:

  • Have the ability to be enabled/disabled
  • Will run when a file is saved
  • Will provide insights on the direct context of the work I am doing (only analyze the current file)
  • Will run a prompt that guides the focus to provide the insights that I want, eliminating noise
  • Open in a new tab in the editor so I do not have to switch windows/leave my current workflow to get the insights

A few details to consider.

  1. You will need a Claude code pro subscription to utilize the API the way I do in this thread.
  2. I built this for sublime text, which is my editor of choice because of how easily I find it to customize to the workflows i want to add.

Step 1: Install Claude Code

npm install -g @anthropic-ai/claude-code

Then verify it is installed.

claude-code --version

Step 2: Authenticate Claude Code

claude-code auth

Step 3: Configure Sublime Text to Use Claude Code

I will be utilize the `terminus` package to leverage Claude code from the terminal.

Install Terminus package (better terminal in Sublime):

  • Press Ctrl+Shift+P / Cmd+Shift+P
  • Type "Package Control: Install Package"
  • Search for "Terminus" and install

Now configure a key binding to trigger terminus to open and run the Claude code command. Go to settings -> key bindings and drop this code in.

    {
        "keys": ["ctrl+alt+t"],
        "command": "terminus_open",
        "args": {
            "cmd": ["/bin/zsh", "-i", "-c", "claude"],
            "cwd": "${file_path:${folder}}"
        }
    },

This command is saying, ""start zsh interactively and run the Claude command". Passing in the cwd ensures Claude has context about the current project/folder.

I also want to add key bindings for enabling/disabiling the plugin.

    {
        "keys": ["ctrl+alt+w"],
        "command": "toggle_claude_watcher",
        "caption": "Toggle Claude Watcher"
    },

Create Custom Plugin

Create a plugin for more seamless integration:

  • Go to Tools → Developer → New Plugin
  • My current plugin code:
import sublime
import sublime_plugin
import os
from threading import Timer
from datetime import datetime

# Global toggle state
claude_watcher_enabled = False

class ClaudeFileWatcher(sublime_plugin.EventListener):
    WATCHED_EXTENSIONS = ['.rb', '.tsx', 'js']
    
    WATCHED_PROJECTS = [
        '/replace with your path/documents/projects/*',
        'your_app_here'
    ]
    
    DEBOUNCE_DELAY = 1.0
    LOG_DIRECTORY = os.path.expanduser("~/Documents/projects/claude_logs")
    pending_timer = None
    
    def on_post_save_async(self, view):
        global claude_watcher_enabled
        
        if not claude_watcher_enabled:
            return
        
        file_path = view.file_name()
        if not file_path:
            return
        
        normalized_path = os.path.normpath(file_path)
        
        in_watched_project = any(
            os.path.normpath(proj) in normalized_path 
            for proj in self.WATCHED_PROJECTS
        )
        
        if not in_watched_project:
            return
        
        _, ext = os.path.splitext(file_path)
        if ext not in self.WATCHED_EXTENSIONS:
            return
        
        if self.pending_timer:
            self.pending_timer.cancel()
        
        self.pending_timer = Timer(self.DEBOUNCE_DELAY, self.review_file, [view, file_path])
        self.pending_timer.start()
    
    def review_file(self, view, file_path):
        file_name = os.path.basename(file_path)
        working_dir = os.path.dirname(file_path)
        
        sublime.set_timeout(lambda: self.send_to_claude(view, file_name, working_dir), 0)

    def send_to_claude(self, view, file_name, working_dir):
        window = view.window()
        if not window:
            return
        
        timestamp = datetime.now().strftime("%H:%M:%S")
        date_stamp = datetime.now().strftime("%Y-%m-%d")
        
        # Get project/app name from working directory
        folders = window.folders()
        if folders:
            # Use the project folder name (root of what's open in Sublime)
            project_name = os.path.basename(folders[0])
        else:
            # Fallback: traverse up to find a reasonable project root
            # Look for common project markers
            current = working_dir
            for _ in range(5):  # Go up max 5 levels
                if any(os.path.exists(os.path.join(current, marker)) 
                       for marker in ['.git', 'Gemfile', 'package.json', '.ruby-version']):
                    project_name = os.path.basename(current)
                    break
                parent = os.path.dirname(current)
                if parent == current:  # Reached root
                    break
                current = parent
            else:
                project_name = "unknown_project"
        
        # Find and log existing terminal content before closing
        self.log_terminal_content(window, project_name, date_stamp, timestamp)
        
        # Close existing terminal
        window.run_command("terminus_close", {"tag": "claude_watcher"})
        
        # Open fresh terminal in panel
        window.run_command("terminus_open", {
            "cmd": ["/bin/zsh", "-i", "-c", 
                    f"claude '[{timestamp}] I just saved {file_name}. " \
                    "Quick review - any bugs, refactors, security issues, " \
                    "query optimizations or improvements? " \
                    "If none, say \"Looks good\". Be brief.'"],
            "cwd": working_dir,
            "tag": "claude_watcher",
            "pre_window_hooks": [
                ["new_pane", {"direction": "down"}]
            ]
        })

    def log_terminal_content(self, window, project_name, date_stamp, timestamp):
        """Log terminal content before closing"""
        # Find the Claude terminal
        claude_view = None
        for v in window.views():
            if v.settings().get("terminus_view.tag") == "claude_watcher":
                claude_view = v
                break
        
        if not claude_view:
            return  # No terminal to log
        
        # Get all content from the terminal
        content = claude_view.substr(sublime.Region(0, claude_view.size()))
        
        if not content or len(content.strip()) == 0:
            return  # Nothing to log
        
        # Create log directory if it doesn't exist
        os.makedirs(self.LOG_DIRECTORY, exist_ok=True)
        
        # Create log filename: project_name_YYYY-MM-DD.log
        log_filename = f"{project_name}_{date_stamp}.log"
        log_path = os.path.join(self.LOG_DIRECTORY, log_filename)
        
        # Append to log file
        try:
            with open(log_path, 'a', encoding='utf-8') as f:
                f.write(f"\n{'='*80}\n")
                f.write(f"Session at {timestamp}\n")
                f.write(f"{'='*80}\n")
                f.write(content)
                f.write(f"\n{'='*80}\n\n")
            
            print(f"Logged Claude session to: {log_path}")
        except Exception as e:
            print(f"Error logging Claude session: {e}")
    
    def find_claude_terminal(self, window):
        """Find existing Claude Watcher terminal view"""
        for view in window.views():
            # Check if this is a Terminus view with our tag
            settings = view.settings()
            if settings.get("terminus_view.tag") == "claude_watcher":
                return view
            # Also check by title as fallback
            if view.name() == "Claude Watcher":
                return view
        return None


class ToggleClaudeWatcherCommand(sublime_plugin.ApplicationCommand):
    def run(self):
        global claude_watcher_enabled
        claude_watcher_enabled = not claude_watcher_enabled
        
        status = "enabled ✓" if claude_watcher_enabled else "disabled ✗"
        sublime.status_message(f"Claude Watcher {status}")


class ClaudeWatcherStatusCommand(sublime_plugin.ApplicationCommand):
    def run(self):
        global claude_watcher_enabled
        watcher = ClaudeFileWatcher()
        
        status = "enabled ✓" if claude_watcher_enabled else "disabled ✗"
        
        message = f"""
Claude Watcher Status: {status}

Settings:
- Debounce delay: {watcher.DEBOUNCE_DELAY} seconds
- Watched projects: {', '.join(watcher.WATCHED_PROJECTS)}
- Watched extensions: {', '.join(watcher.WATCHED_EXTENSIONS)}
        """
        
        sublime.message_dialog(message.strip())

I'm not going to discuss everything in here but want to point out the main takeaways as I see them.

Enabling/Disabling

With the key binding added I can now toggle it on/off and see that status in my code editor

Running the command on save, watcher style.

On save it will pass the command to Claude code:
`I just saved _app.tsx. Quick review - any bugs, refactors, security issues, query optimizations or improvements? If none, say "Looks good". Be brief.`

That command gets passed to Claude in a new panel in sublime text. Example below when adding Goggle analytics to my website.

Worth noting here that when disabled this window will not open, I can just save the file and move on as is.

There is an feature in the python code I am leaving out of the discussion here. Primarily that I also write the output to a log file. This is for my own benefit. Having the output allows me to process those logs and get other details from them after a session. One other reason is that in order to continually send the command to terminus I needed to open and close the window which loses the code we were discussing so this also allows me to keep track of everything that comes up.