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:
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
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.
Use Case: Feature Branch Workflow
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
Group related commands : Put dev, test, and build commands together
Use command indices consistently : Always use terminal 1 for dev server, terminal 2 for tests, etc.
Document complex commands : Add comments explaining what each command does
Keep commands project-specific : Don’t reuse the same commands across projects if they do different things
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 ))