Claude Code workflow
This guide addresses the specific workflow and challenges when developing this Obsidian plugin with Claude Code.
Table of Contents
Section titled “Table of Contents”- Documentation Organization
- What Claude Code Can and Cannot See
- Conversation Migration
- Avoiding Permission Issues
- Alternative Testing Workflows
- Debug Logging Strategy
- Best Practices
Documentation Organization
Section titled “Documentation Organization”This project uses a specific structure for LLM-friendly documentation:
.claude/ Folder (Internal - NOT in GitHub)
Section titled “.claude/ Folder (Internal - NOT in GitHub)”Purpose: Personal context, internal notes, files for you to respond to during development
What goes here:
- Personal Q&A and feedback (e.g.,
RESPONSE_TO_FEEDBACK.md) - Development notes specific to your vault setup
- Internal implementation plans with personal context
- Draft documentation that references personal file paths
- Conversation context you want preserved but not shared
Excluded from GitHub: The .claude/ folder is in .gitignore
Root Folder - ALL CAPS Files (Public - FOR GitHub)
Section titled “Root Folder - ALL CAPS Files (Public - FOR GitHub)”Purpose: Useful documentation for other developers and their LLMs
What goes here:
CLAUDE_CODE_WORKFLOW.md- This file! Workflow guidance for LLM developmentENVIRONMENT_SETUP.md- Setup instructions for any developerTESTING_GUIDE.md- How to test the pluginCONTRIBUTING.md- Standard GitHub contribution guidelinesREADME.md- Standard GitHub readme
Benefits:
- All caps makes these files obvious and easy to find
- Other developers using Claude Code (or other LLMs) can benefit from your workflow
- Generic examples (no personal paths or vault-specific details)
- Helps establish best practices for Obsidian plugin development with AI
Rule of Thumb
Section titled “Rule of Thumb”Ask yourself: “Would another developer using an LLM find this helpful?”
- Yes → Root folder (ALL CAPS filename)
- No (personal context) →
.claude/folder
What Claude Code Can and Cannot See
Section titled “What Claude Code Can and Cannot See”What I CAN See ✅
Section titled “What I CAN See ✅”- File contents: Any file you ask me to read or that I read during development
- Command output: Output from bash commands I run (npm, bun, tsc, git, etc.)
- Test results: Output from
bun test, TypeScript compilation errors - Build output: esbuild errors and warnings
- File structure: Directory listings, file searches
- Log files: If the plugin writes to log files, I can read them
What I CANNOT See ❌
Section titled “What I CANNOT See ❌”- Obsidian Developer Console: I cannot see
console.log()output when you run Obsidian - Obsidian UI: I cannot see the actual plugin UI or how it renders
- Runtime behavior: I cannot see how the plugin behaves when you interact with it
- Obsidian’s internal state: Vault files, metadata cache, etc. (unless you share them)
- Your screen: I cannot see screenshots unless you explicitly share them
Implications
Section titled “Implications”Because I cannot see Obsidian’s console:
- You need to report errors: Copy console errors/warnings and paste them to me
- Debug logs should write to files: We can set up file-based logging I can read
- UI testing is manual: You’ll need to test the UI and report what you see
- Integration testing requires your input: You’ll need to describe behavior
Conversation Migration
Section titled “Conversation Migration”Sometimes you need to move a Claude Code conversation to a different directory (e.g., to avoid permission issues or switch environments).
The Gist Approach
Section titled “The Gist Approach”Based on gwpl’s conversation migration script, here’s how to migrate a conversation:
Step 1: Locate Your Conversation
Section titled “Step 1: Locate Your Conversation”Claude Code stores conversations in ~/.claude/conversations/.
# List recent conversationsls -lt ~/.claude/conversations/ | head -10
# Find conversations by looking at .claude.jsoncat ~/.claude.json | grep -A 5 '"currentDirectory"'Step 2: Identify the Conversation ID
Section titled “Step 2: Identify the Conversation ID”Each conversation has a unique ID. Find it by:
# Your current conversation ID is in your working directorycat .claude.json 2>/dev/null | grep conversationId
# Or check Claude Code's main configcat ~/.claude/.claude.json | grep conversationIdStep 3: Copy Conversation to New Directory
Section titled “Step 3: Copy Conversation to New Directory”# Example: Moving from Windows path to WSL homeOLD_DIR="/mnt/c/Users/YourUsername/Documents/vault/plugin_development/dynamic-tags-folders-plugin"NEW_DIR="$HOME/projects/dynamic-tags-folders-plugin"
# Create new directorymkdir -p "$NEW_DIR"
# Copy project files (not node_modules!)cd "$OLD_DIR"rsync -av --exclude='node_modules' --exclude='.git' . "$NEW_DIR/"
# Or use git clone if it's a repocd "$HOME/projects"git clone <repo-url> dynamic-tags-folders-pluginStep 4: Start Fresh in New Directory
Section titled “Step 4: Start Fresh in New Directory”Instead of migrating the conversation (which is complex), it’s often simpler to:
- Summarize the current state in a message to me
- Start a new conversation in the new directory
- Reference the summary so I have context
Example summary template:
I'm continuing work on the Obsidian Dynamic Tags & Folders plugin.Current state:- 156 tests passing- TypeScript compilation working- UI components built (RuleEditorModal, SettingsTab)- Pending: Sync engine implementation- Issue: esbuild platform mismatch, need to reinstall deps
Project location: ~/projects/dynamic-tags-folders-pluginEnvironment: WSL / PowerShell / LinuxAutomated Migration Script
Section titled “Automated Migration Script”If you frequently need to migrate, create this helper script:
#!/bin/bashOLD_DIR="$1"NEW_DIR="$2"
if [ -z "$OLD_DIR" ] || [ -z "$NEW_DIR" ]; then echo "Usage: migrate-claude-project.sh <old-dir> <new-dir>" exit 1fi
echo "Migrating from: $OLD_DIR"echo "Migrating to: $NEW_DIR"
# Create new directorymkdir -p "$NEW_DIR"
# Copy files excluding heavy directoriesrsync -av \ --exclude='node_modules' \ --exclude='.git' \ --exclude='dist' \ --exclude='build' \ "$OLD_DIR/" "$NEW_DIR/"
echo "Migration complete!"echo "Now run: cd \"$NEW_DIR\" && bun install"Usage:
chmod +x ~/bin/migrate-claude-project.sh~/bin/migrate-claude-project.sh \ "/mnt/c/Users/YourUsername/Documents/vault/plugin_development/dynamic-tags-folders-plugin" \ "$HOME/projects/dynamic-tags-folders-plugin"Avoiding Permission Issues
Section titled “Avoiding Permission Issues”The biggest pain point with WSL + Claude Code is permission mismatches.
Root Cause
Section titled “Root Cause”Claude Code may run as:
rootuser (via sudo)- Different user than your WSL user
- With different environment variables
This causes files created by Claude Code to be owned by root, making them hard to modify.
Solution 1: Work in WSL Home Directory (RECOMMENDED)
Section titled “Solution 1: Work in WSL Home Directory (RECOMMENDED)”Instead of working in /mnt/c/Users/..., work in your WSL home directory:
# Bad (Windows filesystem, permission issues)/mnt/c/Users/YourUsername/Documents/vault/plugin_development/dynamic-tags-folders-plugin
# Good (WSL filesystem, no permission issues)~/projects/dynamic-tags-folders-pluginSteps:
- Clone or copy the project to
~/projects/:
cd ~/projectsgit clone https://github.com/cybersader/obsidian-tag-and-folder-mapper.git dynamic-tags-folders-plugincd dynamic-tags-folders-pluginbun install-
Start a new Claude Code session in
~/projects/dynamic-tags-folders-plugin -
Access from Windows if needed:
# In Windows Explorer, navigate to:\\wsl$\Ubuntu\home\<your-username>\projects\dynamic-tags-folders-pluginSolution 2: Fix Permissions After the Fact
Section titled “Solution 2: Fix Permissions After the Fact”If you’re already working in /mnt/c/ and files are owned by root:
# Fix ownershipsudo chown -R $USER:$USER .
# Make files writablechmod -R u+w .
# Reinstall dependencies in your user contextrm -rf node_modulesbun installSolution 3: Don’t Run Claude Code with Sudo
Section titled “Solution 3: Don’t Run Claude Code with Sudo”If you’re running Claude Code with sudo claude-code, stop doing that:
# Badsudo claude-code
# Goodclaude-codeIf Claude Code requires sudo for some reason, that’s a configuration issue to fix, not a workflow to maintain.
Solution 4: Separate Environments Completely
Section titled “Solution 4: Separate Environments Completely”Keep WSL and Windows development completely separate:
# WSL environment~/projects/dynamic-tags-folders-plugin/ - Work here with Claude Code - Run tests, builds - All file operations
# Link to Obsidian vault (for testing)ln -s ~/projects/dynamic-tags-folders-plugin \ "/mnt/c/Users/YourUsername/Documents/YourVault/.obsidian/plugins/dynamic-tags-folders"Alternative Testing Workflows
Section titled “Alternative Testing Workflows”Since I cannot see Obsidian’s console, here are alternative testing approaches:
Workflow 1: File-Based Debug Logging
Section titled “Workflow 1: File-Based Debug Logging”Modify the plugin to write debug logs to a file I can read:
import { App } from 'obsidian';import * as fs from 'fs';import * as path from 'path';
export class DebugLogger { private logPath: string;
constructor(app: App) { // Write to vault's root directory this.logPath = path.join(app.vault.adapter.basePath, 'plugin-debug.log'); }
log(message: string, data?: any) { const timestamp = new Date().toISOString(); const logEntry = `[${timestamp}] ${message}${data ? '\n' + JSON.stringify(data, null, 2) : ''}\n`;
fs.appendFileSync(this.logPath, logEntry); console.log(message, data); // Also log to console }
clear() { fs.writeFileSync(this.logPath, ''); }}
// Usage in main.tsimport { DebugLogger } from './utils/debug';
export default class DynamicTagsFoldersPlugin extends Plugin { debugLogger: DebugLogger;
async onload() { if (this.settings.debugMode) { this.debugLogger = new DebugLogger(this.app); this.debugLogger.clear(); // Clear old logs this.debugLogger.log('Plugin loaded'); } }}Then I can read the log file:
# I can read this file to see what's happeningcat "/mnt/c/Users/YourUsername/Documents/YourVault/plugin-debug.log"Workflow 2: Unit Test Everything
Section titled “Workflow 2: Unit Test Everything”Instead of relying on UI testing, write comprehensive unit tests:
import { describe, test, expect } from 'bun:test';import { FolderToTagSync } from './folder-to-tag';
describe('FolderToTagSync', () => { test('applies folder name as tag', () => { const sync = new FolderToTagSync(/* ... */); const result = sync.syncFile('Projects/MyProject/note.md');
expect(result.tags).toContain('MyProject'); });});Benefits:
- I can run
bun testand see results - No need for Obsidian to be running
- Faster development iteration
Workflow 3: Console Error Reporting
Section titled “Workflow 3: Console Error Reporting”When you test in Obsidian:
- Open Obsidian Developer Console:
Ctrl+Shift+I(Windows) orCmd+Option+I(Mac) - Test the plugin feature
- Copy any errors/logs
- Paste them in our conversation
Example:
User: I tested creating a new rule and got this error:
TypeError: Cannot read property 'folderTransforms' of undefined at RuleEditorModal.onOpen (main.js:234) at Modal.open (app.js:1234)Workflow 4: Emulate UI Interactions in Tests
Section titled “Workflow 4: Emulate UI Interactions in Tests”Instead of testing through the UI, test the underlying logic:
import { describe, test, expect } from 'bun:test';import { Rule, RuleDirection } from '../types/settings';
describe('Rule Creation Logic', () => { test('creates valid folder-to-tag rule', () => { const rule: Rule = { id: 'test-rule', name: 'Test Rule', enabled: true, direction: 'folder-to-tag', pattern: { type: 'path', value: 'Projects/**' }, folderTransforms: { caseTransform: 'none' }, tagTransforms: { caseTransform: 'kebab-case' }, options: { createFolders: false, addTags: true, removeOrphanedTags: false, syncOnFileCreate: true, syncOnFileMove: true, syncOnFileRename: true } };
// Test that rule is valid expect(rule.direction).toBe('folder-to-tag'); expect(rule.options.addTags).toBe(true); });});Workflow 5: Incremental Testing
Section titled “Workflow 5: Incremental Testing”Break down testing into small, verifiable steps:
- You: “I’m going to test creating a rule through the UI”
- You: “The modal opened successfully, I see all the fields”
- You: “I filled in: name=‘Test’, direction=‘folder-to-tag’, pattern=‘/Projects’”
- You: “Clicking Save gave me an error: [paste error]”
- Me: “I see the issue, let me fix it…” [makes fix]
- You: “Testing again… it worked!”
This gives me visibility without needing console access.
Debug Logging Strategy
Section titled “Debug Logging Strategy”Here’s a comprehensive debug logging setup:
1. Create Debug Utility
Section titled “1. Create Debug Utility”import { App, normalizePath } from 'obsidian';
export class DebugLogger { private logPath: string; private enabled: boolean;
constructor(private app: App, enabled: boolean = false) { this.enabled = enabled; // Use vault's config directory this.logPath = normalizePath( `${app.vault.configDir}/plugins/dynamic-tags-folders/debug.log` ); }
async log(level: 'info' | 'warn' | 'error', message: string, data?: any) { if (!this.enabled) return;
const timestamp = new Date().toISOString(); const dataStr = data ? '\n' + JSON.stringify(data, null, 2) : ''; const logEntry = `[${timestamp}] [${level.toUpperCase()}] ${message}${dataStr}\n`;
// Write to file try { const adapter = this.app.vault.adapter; const existing = await adapter.read(this.logPath).catch(() => ''); await adapter.write(this.logPath, existing + logEntry); } catch (e) { console.error('Failed to write debug log:', e); }
// Also log to console console[level](message, data); }
async clear() { if (!this.enabled) return;
try { await this.app.vault.adapter.write(this.logPath, ''); } catch (e) { console.error('Failed to clear debug log:', e); } }
info(message: string, data?: any) { return this.log('info', message, data); } warn(message: string, data?: any) { return this.log('warn', message, data); } error(message: string, data?: any) { return this.log('error', message, data); }}2. Integrate with Plugin
Section titled “2. Integrate with Plugin”import { DebugLogger } from './utils/debug';
export default class DynamicTagsFoldersPlugin extends Plugin { debugLogger: DebugLogger;
async onload() { await this.loadSettings();
this.debugLogger = new DebugLogger( this.app, this.settings.pluginOptions.debugMode );
await this.debugLogger.clear(); await this.debugLogger.info('Plugin loaded', { version: this.manifest.version, rulesCount: this.settings.rules.length });
// ... rest of onload }}3. Use Throughout Codebase
Section titled “3. Use Throughout Codebase”// Example: In sync engineexport class FolderToTagSync { constructor( private app: App, private settings: DynamicTagsFoldersSettings, private logger: DebugLogger ) {}
async syncFile(file: TFile) { await this.logger.info('Starting folder-to-tag sync', { file: file.path, folder: file.parent?.path });
try { // ... sync logic await this.logger.info('Sync completed successfully'); } catch (error) { await this.logger.error('Sync failed', { error: error.message, stack: error.stack }); } }}4. Reading Debug Logs
Section titled “4. Reading Debug Logs”I can read the debug log file:
# Read the entire logcat "/mnt/c/Users/YourUsername/Documents/YourVault/.obsidian/plugins/dynamic-tags-folders/debug.log"
# Watch log in real-time (as you test)tail -f "/mnt/c/Users/YourUsername/Documents/YourVault/.obsidian/plugins/dynamic-tags-folders/debug.log"
# Filter for errors onlygrep ERROR "/mnt/c/Users/YourUsername/Documents/YourVault/.obsidian/plugins/dynamic-tags-folders/debug.log"Best Practices
Section titled “Best Practices”1. Clear Division of Responsibilities
Section titled “1. Clear Division of Responsibilities”Claude Code handles:
- TypeScript compilation
- Running tests
- Code generation
- Refactoring
- Documentation
- Reading log files
You handle:
- UI testing in Obsidian
- Final builds (if platform issues)
- Git commits
- Reporting console errors
- Testing edge cases
2. Communication Protocol
Section titled “2. Communication Protocol”When reporting issues, include:
What you did: [Clicked "Create Rule" button]What you expected: [Rule editor modal to open]What happened: [Nothing happened, console shows error]Error message: [paste full error from console]Environment: [WSL/PowerShell/Linux]Plugin version: [latest from main.ts]3. Iterative Development
Section titled “3. Iterative Development”- Me: Implement feature
- Me: Write unit tests
- Me: Run tests, ensure they pass
- You: Test in Obsidian UI
- You: Report any issues
- Me: Fix issues
- Repeat until feature works
4. Use Platform-Aware Scripts
Section titled “4. Use Platform-Aware Scripts”Always use the platform-aware build script:
# Instead of:bun run build
# Use:node scripts/build.mjsThis automatically handles platform detection.
5. Keep Environments Separate
Section titled “5. Keep Environments Separate”Don’t mix WSL and PowerShell in the same project directory:
# BAD: Installing in WSL, building in PowerShellcd /mnt/c/Users/YourUsername/Documents/vault/plugin_developmentbun install # WSL# Then in PowerShell: bun run build # ERROR: platform mismatch
# GOOD: Pick one environment per projectcd ~/projects/project # WSL onlybun installbun run build6. Regular Permission Checks
Section titled “6. Regular Permission Checks”Periodically verify file ownership:
# Check ownershipls -la | grep root
# If you see root-owned files, fix:sudo chown -R $USER:$USER .7. Leverage Version Control
Section titled “7. Leverage Version Control”Commit working states frequently:
# After each feature worksgit add .git commit -m "feat: implement folder-to-tag sync"
# This makes it easy to revert if something breaksgit log --onelinegit reset --hard <commit-hash>Quick Reference
Section titled “Quick Reference”Migrating Conversation
Section titled “Migrating Conversation”# 1. Summarize state in Claude Code# 2. Copy project to new locationrsync -av --exclude='node_modules' old/ new/
# 3. Start fresh session in new locationcd new/bun installFixing Permissions
Section titled “Fixing Permissions”sudo chown -R $USER:$USER .rm -rf node_modulesbun installReading Debug Logs
Section titled “Reading Debug Logs”cat "/mnt/c/path/to/vault/.obsidian/plugins/dynamic-tags-folders/debug.log"tail -f "/mnt/c/path/to/vault/.obsidian/plugins/dynamic-tags-folders/debug.log"Platform-Aware Build
Section titled “Platform-Aware Build”node scripts/build.mjsSummary
Section titled “Summary”Key Takeaways:
- I cannot see Obsidian console - Use file-based logging or report errors manually
- Work in WSL home directory - Avoids permission issues with
/mnt/c/ - Migrate conversations by summarizing state - Simpler than technical migration
- Use debug logging to file - Gives me visibility into runtime behavior
- Test incrementally - Small steps with clear reporting
- Keep environments separate - WSL OR PowerShell, not both
With these workflows, we can develop effectively even with Claude Code’s limitations.
Next Steps
Section titled “Next Steps”- Decide on environment: Work in
~/projects/(WSL) or native PowerShell? - Set up debug logging: Add DebugLogger utility
- Create test vault: See TESTING_GUIDE.md
- Start testing: Test existing UI, report any issues
- Implement sync engine: Next major feature
Let me know which environment you prefer, and we can proceed!