Skip to main content

Overview

Project settings allow you to configure Harpoon differently for each project or directory. This is useful for:
  • Storing frequently-used terminal commands per project
  • Managing different mark sets for different codebases
  • Automating project-specific workflows
Project settings are configured in the projects table:
require("harpoon").setup({
    projects = {
        ["/path/to/project"] = {
            term = { cmds = { ... } },
            mark = { marks = { ... } }
        }
    }
})

Project Structure

Each project is keyed by its absolute directory path. The path is automatically expanded, so ~ and environment variables like $HOME work:
projects = {
    -- Absolute path
    ["/home/user/projects/my-app"] = { ... },
    
    -- Using $HOME
    ["$HOME/personal/vim-with-me/server"] = { ... },
    
    -- Using ~
    ["~/work/api-server"] = { ... },
}
Paths are normalized and expanded at runtime. ~/projects/app and $HOME/projects/app refer to the same project.

Terminal Commands

The most common use of project settings is defining terminal commands that can be sent to terminals:
projects[path].term.cmds
table
default:"{}"
Array of command strings that can be sent to terminals using sendCommand(terminal_id, command_index).

Basic Example

From the Harpoon README (lines 173-183):
require("harpoon").setup({
    projects = {
        ["$HOME/personal/vim-with-me/server"] = {
            term = {
                cmds = {
                    "./env && npx ts-node src/index.ts"
                }
            }
        }
    }
})
Now you can execute this command in a terminal:
-- Opens/focuses terminal 1 and sends command 1
require("harpoon.term").sendCommand(1, 1)

Multiple Commands Per Project

Store all your frequently-used commands:
projects = {
    ["~/projects/web-app"] = {
        term = {
            cmds = {
                "npm run dev",           -- Command 1
                "npm test -- --watch",   -- Command 2
                "npm run build",         -- Command 3
                "npm run lint",          -- Command 4
                "docker-compose up",     -- Command 5
            }
        }
    }
}
Send any command to any terminal:
-- Send "npm run dev" to terminal 1
require("harpoon.term").sendCommand(1, 1)

-- Send "npm test -- --watch" to terminal 2
require("harpoon.term").sendCommand(2, 2)

-- Send "docker-compose up" to terminal 1
require("harpoon.term").sendCommand(1, 5)
Map commands to keys for quick access:
-- Run dev server in terminal 1
vim.keymap.set("n", "<leader>td", function()
    require("harpoon.term").sendCommand(1, 1)
end)

-- Run tests in terminal 2
vim.keymap.set("n", "<leader>tt", function()
    require("harpoon.term").sendCommand(2, 2)
end)

-- Build in terminal 3
vim.keymap.set("n", "<leader>tb", function()
    require("harpoon.term").sendCommand(3, 3)
end)

Multiple Projects

Configure commands for multiple projects:
require("harpoon").setup({
    projects = {
        ["~/projects/frontend"] = {
            term = {
                cmds = {
                    "npm run dev",
                    "npm test",
                    "npm run storybook"
                }
            }
        },
        
        ["~/projects/backend"] = {
            term = {
                cmds = {
                    "go run cmd/server/main.go",
                    "go test ./...",
                    "make docker-build"
                }
            }
        },
        
        ["~/projects/infrastructure"] = {
            term = {
                cmds = {
                    "terraform plan",
                    "terraform apply",
                    "kubectl get pods"
                }
            }
        }
    }
})
Harpoon automatically uses the correct project config based on your current working directory.

Mark Configuration

projects[path].mark.marks
table
default:"{}"
Array of file marks for this project. Usually managed automatically through the UI, but can be pre-configured.

Manual Mark Configuration

You can pre-configure marks for a project:
projects = {
    ["~/projects/my-app"] = {
        mark = {
            marks = {
                { filename = "src/main.lua" },
                { filename = "src/config.lua" },
                { filename = "src/utils.lua" },
            }
        }
    }
}
Manually configuring marks is rarely needed. It’s better to add marks interactively using require("harpoon.mark").add_file(). Harpoon will save them automatically.

Branch-Specific Configuration

When mark_branch = true is set in global settings, marks are stored per git branch within each project:
require("harpoon").setup({
    global_settings = {
        mark_branch = true,  -- Enable per-branch marks
    },
    projects = {
        ["~/projects/app"] = {
            term = {
                cmds = { "npm run dev" }
            }
        }
    }
})

How Branch-Based Marks Work

Internal structure (you don’t need to configure this manually):
-- With mark_branch = false (default)
projects = {
    ["/home/user/projects/app"] = {
        mark = { marks = { ... } }  -- Shared across all branches
    }
}

-- With mark_branch = true
projects = {
    ["/home/user/projects/app__branch__main"] = {
        mark = { marks = { ... } }  -- Marks for main branch
    },
    ["/home/user/projects/app__branch__feature/auth"] = {
        mark = { marks = { ... } }  -- Marks for feature/auth branch
    }
}
Harpoon handles this automatically based on your current git branch.
You’re working on multiple features simultaneously:Branch: main
  • Marks: README.md, package.json, src/index.ts
Branch: feature/authentication
  • Marks: src/auth/login.ts, src/auth/signup.ts, src/middleware/auth.ts
Branch: feature/api-integration
  • Marks: src/api/client.ts, src/api/types.ts, src/hooks/useApi.ts
With mark_branch = true, switching branches automatically loads the relevant marks for that feature.

Complete Project Configuration

Here’s a comprehensive example combining all features:
require("harpoon").setup({
    global_settings = {
        save_on_change = true,
        mark_branch = false,  -- Use project-based marks
    },
    
    projects = {
        -- Full-stack web application
        ["~/work/ecommerce-app"] = {
            term = {
                cmds = {
                    "npm run dev",              -- 1: Start dev server
                    "npm test -- --watch",      -- 2: Watch tests
                    "npm run build",            -- 3: Production build
                    "npm run lint:fix",         -- 4: Auto-fix linting
                    "docker-compose up -d",     -- 5: Start services
                    "docker-compose logs -f",   -- 6: Follow logs
                }
            }
        },
        
        -- Go microservice
        ["~/work/payment-service"] = {
            term = {
                cmds = {
                    "go run cmd/server/main.go",
                    "go test -v ./...",
                    "make proto-gen",
                    "docker build -t payment-svc .",
                }
            }
        },
        
        -- Python data pipeline
        ["~/personal/data-pipeline"] = {
            term = {
                cmds = {
                    "poetry run python src/main.py",
                    "poetry run pytest tests/ -v",
                    "poetry run black .",
                    "poetry run mypy src/",
                }
            }
        },
        
        -- Neovim config
        ["~/.config/nvim"] = {
            term = {
                cmds = {
                    "nvim --headless -c 'Lazy sync' -c 'qa'",
                    "nvim --headless -c 'TSUpdate' -c 'qa'",
                }
            }
        },
    }
})

Dynamic Project Configuration

Generate project configs dynamically:
local function create_node_project(path)
    return {
        [path] = {
            term = {
                cmds = {
                    "npm run dev",
                    "npm test",
                    "npm run build",
                }
            }
        }
    }
end

local projects = {}
vim.tbl_extend("force", projects, create_node_project("~/work/app-1"))
vim.tbl_extend("force", projects, create_node_project("~/work/app-2"))

require("harpoon").setup({ projects = projects })

Viewing Commands Menu

Harpoon provides a UI to view and execute your configured commands:
-- Open the commands menu
require('harpoon.cmd-ui').toggle_quick_menu()
This shows all commands for the current project. You can select and send them to terminals.

Using with Tmux

All project commands work with tmux terminals too:
projects = {
    ["~/projects/api"] = {
        term = {
            cmds = { "go run main.go" }
        }
    }
}
Send to tmux window instead of terminal:
-- Send to tmux window 1
require("harpoon.tmux").sendCommand(1, 1)

-- Send to specific tmux pane
require("harpoon.tmux").sendCommand("%3", 1)

Storage Location

Project configurations from setup() are merged with persisted data:
  • Setup config: Defined in your Neovim config (Lua)
  • Persisted data: Stored in ~/.local/share/nvim/harpoon.json (JSON)
  • User overrides: Can be in ~/.config/nvim/harpoon.json (JSON)
The final configuration is a merge of all three sources.
Terminal commands must be defined in setup() or in JSON config files. Marks are usually managed through the UI and automatically persisted.

Best Practices

  1. Group related commands: Put dev, test, and build commands together
  2. Use command indices consistently: Always use terminal 1 for dev server, terminal 2 for tests, etc.
  3. Document complex commands: Add comments explaining what each command does
  4. Keep commands project-specific: Don’t reuse the same commands across projects if they do different things
  5. Use environment variables: Reference $HOME, $USER, etc. for portability

Accessing Project Config

Programmatically get the current project’s config:
-- Get terminal config for current project
local term_config = require("harpoon").get_term_config()
print(vim.inspect(term_config.cmds))

-- Get mark config for current project
local mark_config = require("harpoon").get_mark_config()
print(vim.inspect(mark_config.marks))

Build docs developers (and LLMs) love