Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Scaffold a new Nav 2 plugin (controller, planner, behavior, smoother, goal-checker, progress-checker, costmap layer, or BT node). Wires pluginlib registration, parameter declaration on the lifecycle node, and a minimal integration test. Trigger when the user asks to write or extend a Nav 2 plugin.
.claude/skills/harunkurtdev-new-nav2-plugin/SKILL.md| Model | Eval pass | Runs |
|---|---|---|
| gemini-3.6-flash | 100% | 1 |
| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-02 | ✗→✓ | ▲ Improved | 35% | 0% |
| case-03 | ✗→✓ | ▲ Improved | 179% | 0% |
| case-10 | ✗→✓ | ▲ Improved | 135% | 0% |
| case-18 | ✗→✓ | ▲ Improved | 314% | 0% |
| case-01 | ✓→✓ | = Same ✓ | 23% | 0% |
Nav 2 exposes every behaviour as a pluginlib-loaded class behind a small set of nav2_core::* (or nav2_costmap_2d::Layer, or BT::ActionNodeBase) interfaces. The recipe is the same shape for every plugin kind — only the base class and the host server change.
| Plugin kind | Base class | Loaded by | Plugin description file | |---------------------|---------------------------------------------|----------------------------|-------------------------| | Controller | nav2_core::Controller | controller_server | nav2_core_plugins.xml | | Global planner | nav2_core::GlobalPlanner | planner_server | nav2_core_plugins.xml | | Smoother | nav2_core::Smoother | smoother_server | nav2_core_plugins.xml | | Goal checker | nav2_core::GoalChecker | controller_server | nav2_core_plugins.xml | | Progress checker | nav2_core::ProgressChecker | controller_server | nav2_core_plugins.xml | | Behavior | nav2_core::Behavior | behavior_server | nav2_core_plugins.xml | | Costmap layer | nav2_costmap_2d::Layer / CostmapLayer | local_/global_costmap | nav2_costmap_2d_plugins.xml | | BT action / cond. | BT::SyncActionNode / BT::ConditionNode | bt_navigator | nav2_tree_nodes.xml |
See .claude/skills/nav2_core_interfaces/SKILL.md for the full method list of each base.
For a controller (other plugin kinds are analogous):
cpp// include/<pkg>/<snake_class>.hpp #ifndef <PKG>__<SNAKE_CLASS>_HPP_ #define <PKG>__<SNAKE_CLASS>_HPP_ #include <memory> #include <string> #include <nav2_core/controller.hpp> #include <rclcpp/rclcpp.hpp> #include <rclcpp_lifecycle/lifecycle_node.hpp> namespace <pkg> { class <ClassName> : public nav2_core::Controller { public: <ClassName>() = default; ~<ClassName>() override = default; void configure( const rclcpp_lifecycle::LifecycleNode::WeakPtr & parent, std::string name, std::shared_ptr<tf2_ros::Buffer> tf, std::shared_ptr<nav2_costmap_2d::Costmap2DROS> costmap_ros) override; void cleanup() override; void activate() override; void deactivate() override; geometry_msgs::msg::TwistStamped computeVelocityCommands( const geometry_msgs::msg::PoseStamped & pose, const geometry_msgs::msg::Twist & velocity, nav2_core::GoalChecker * goal_checker) override; void setPlan(const nav_msgs::msg::Path & path) override; void setSpeedLimit(const double & speed_limit, const bool & percentage) override; private: rclcpp_lifecycle::LifecycleNode::WeakPtr node_; std::string plugin_name_; std::shared_ptr<tf2_ros::Buffer> tf_; std::shared_ptr<nav2_costmap_2d::Costmap2DROS> costmap_ros_; // Declared params double desired_linear_vel_{0.0}; }; } // namespace <pkg> #endif
cpp// src/<snake_class>.cpp #include "<pkg>/<snake_class>.hpp" #include <pluginlib/class_list_macros.hpp> namespace <pkg> { void <ClassName>::configure( const rclcpp_lifecycle::LifecycleNode::WeakPtr & parent, std::string name, std::shared_ptr<tf2_ros::Buffer> tf, std::shared_ptr<nav2_costmap_2d::Costmap2DROS> costmap_ros) { node_ = parent; plugin_name_ = name; tf_ = tf; costmap_ros_ = costmap_ros; auto node = node_.lock(); nav2_util::declare_parameter_if_not_declared( node, plugin_name_ + ".desired_linear_vel", rclcpp::ParameterValue(0.5)); node->get_parameter(plugin_name_ + ".desired_linear_vel", desired_linear_vel_); } void <ClassName>::cleanup() {} void <ClassName>::activate() {} void <ClassName>::deactivate() {} geometry_msgs::msg::TwistStamped <ClassName>::computeVelocityCommands( const geometry_msgs::msg::PoseStamped & /*pose*/, const geometry_msgs::msg::Twist & /*velocity*/, nav2_core::GoalChecker * /*goal_checker*/) { geometry_msgs::msg::TwistStamped cmd_vel; cmd_vel.header.stamp = node_.lock()->now(); cmd_vel.twist.linear.x = desired_linear_vel_; return cmd_vel; } void <ClassName>::setPlan(const nav_msgs::msg::Path & /*path*/) {} void <ClassName>::setSpeedLimit(const double & /*speed_limit*/, const bool & /*percentage*/) {} } // namespace <pkg> PLUGINLIB_EXPORT_CLASS(<pkg>::<ClassName>, nav2_core::Controller)
Critical points:
nav2_util::declare_parameter_if_not_declared —Nav 2 calls configure more than once across lifecycle transitions.
<plugin_name>.<param> because users setthem under the plugin alias they pick in YAML, not under the plugin class name.
PLUGINLIB_EXPORT_CLASS last argument must match the loader'sexpected base class (nav2_core::Controller, nav2_costmap_2d::Layer, …).
plugins.xml)xml<library path="<pkg>"> <class type="<pkg>::<ClassName>" base_class_type="nav2_core::Controller"> <description> One-line description of what this controller does. </description> </class> </library>
CMakeLists.txtcmakefind_package(ament_cmake REQUIRED) find_package(rclcpp REQUIRED) find_package(rclcpp_lifecycle REQUIRED) find_package(pluginlib REQUIRED) find_package(nav2_core REQUIRED) find_package(nav2_costmap_2d REQUIRED) find_package(nav2_util REQUIRED) find_package(tf2_ros REQUIRED) find_package(geometry_msgs REQUIRED) find_package(nav_msgs REQUIRED) add_library(${PROJECT_NAME} SHARED src/<snake_class>.cpp) target_include_directories(${PROJECT_NAME} PUBLIC $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include> $<INSTALL_INTERFACE:include>) ament_target_dependencies(${PROJECT_NAME} rclcpp rclcpp_lifecycle pluginlib nav2_core nav2_costmap_2d nav2_util tf2_ros geometry_msgs nav_msgs) pluginlib_export_plugin_description_file(nav2_core plugins.xml) install(TARGETS ${PROJECT_NAME} ARCHIVE DESTINATION lib LIBRARY DESTINATION lib RUNTIME DESTINATION bin) install(DIRECTORY include/ DESTINATION include) install(FILES plugins.xml DESTINATION share/${PROJECT_NAME}) ament_export_include_directories(include) ament_export_libraries(${PROJECT_NAME}) ament_export_dependencies(rclcpp rclcpp_lifecycle pluginlib nav2_core) ament_package()
For costmap layers replace nav2_core with nav2_costmap_2d in the pluginlib_export_plugin_description_file call.
package.xmlxml<depend>rclcpp</depend> <depend>rclcpp_lifecycle</depend> <depend>pluginlib</depend> <depend>nav2_core</depend> <depend>nav2_costmap_2d</depend> <depend>nav2_util</depend> <depend>tf2_ros</depend> <depend>geometry_msgs</depend> <depend>nav_msgs</depend>
config/<snake_class>.yaml)yamlcontroller_server: ros__parameters: controller_plugins: ["FollowPath"] FollowPath: plugin: "<pkg>::<ClassName>" desired_linear_vel: 0.5
The key FollowPath is the alias users will set in their nav2 params; the plugin: line is the fully-qualified class name.
cpp// test/integration/test_load_plugin.cpp #include <gtest/gtest.h> #include <pluginlib/class_loader.hpp> #include <nav2_core/controller.hpp> TEST(LoadPlugin, IsConstructible) { pluginlib::ClassLoader<nav2_core::Controller> loader( "nav2_core", "nav2_core::Controller"); auto plugin = loader.createSharedInstance("<pkg>::<ClassName>"); EXPECT_NE(plugin, nullptr); }
bash/build <pkg> /test <pkg>
Then point a Nav 2 stack at the new plugin via the YAML and ros2 launch nav2_bringup tb3_simulation_launch.py.
PLUGINLIB_EXPORT_CLASS — the loader will silently failto find the class.
plugin_name_.placeholders.
nav2_core without nav2_util — nav2_util::declare_* isin a separate target.
updateCosts() — that runs atcostmap rate and must be hot-path-safe.
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-01 | pass→pass | 26,394 | 20,179 | -24% | 1 | 1 | 0% | 6,050 | 7,434 | +23% | 0 | 0 | — |
case-02 | fail→pass | 18,311 | 13,441 | -27% | 1 | 1 | 0% | 4,331 | 5,844 | +35% | 0 | 0 | — |
case-03 | fail→pass | 6,387 | 4,039 | -37% | 1 | 1 | 0% | 1,181 | 3,295 | +179% | 0 | 0 | — |
case-04 | pass→pass | 10,663 | 3,366 | -68% | 1 | 1 | 0% | 2,050 | 3,242 | +58% | 0 | 0 | — |
case-05 | pass→pass | 3,800 | 2,410 | -37% | 1 | 1 | 0% | 632 | 2,921 | +362% | 0 | 0 | — |
case-06 | pass→pass | 4,968 | 2,723 | -45% | 1 | 1 | 0% | 896 | 3,041 | +239% | 0 | 0 | — |
case-07 | pass→pass | 5,876 | 3,156 | -46% | 1 | 1 | 0% | 1,154 | 3,127 | +171% | 0 | 0 | — |
case-08 | pass→pass | 2,720 | 1,734 | -36% | 1 | 1 | 0% | 425 | 2,837 | +568% | 0 | 0 | — |
case-09 | pass→pass | 12,759 | 6,257 | -51% | 1 | 1 | 0% | 2,348 | 3,784 | +61% | 0 | 0 | — |
case-10 | fail→pass | 6,999 | 4,891 | -30% | 1 | 1 | 0% | 1,480 | 3,475 | +135% | 0 | 0 | — |
case-11 | pass→pass | 6,425 | 4,757 | -26% | 1 | 1 | 0% | 1,292 | 3,452 | +167% | 0 | 0 | — |
case-12 | pass→pass | 4,976 | 2,050 | -59% | 1 | 1 | 0% | 904 | 2,899 | +221% | 0 | 0 | — |
case-13 | pass→pass | 6,022 | 3,374 | -44% | 1 | 1 | 0% | 1,032 | 3,262 | +216% | 0 | 0 | — |
case-14 | pass→pass | 8,763 | 4,388 | -50% | 1 | 1 | 0% | 1,554 | 3,383 | +118% | 0 | 0 | — |
case-15 | pass→pass | 11,588 | 7,016 | -39% | 1 | 1 | 0% | 1,904 | 3,675 | +93% | 0 | 0 | — |
case-16 | pass→pass | 7,422 | 4,924 | -34% | 1 | 1 | 0% | 1,214 | 3,321 | +174% | 0 | 0 | — |
case-17 | pass→pass | 9,115 | 4,397 | -52% | 1 | 1 | 0% | 1,726 | 3,434 | +99% | 0 | 0 | — |
case-18 | fail→pass | 3,970 | 1,417 | -64% | 1 | 1 | 0% | 671 | 2,780 | +314% | 0 | 0 | — |
case-19 | pass→pass | 3,879 | 1,523 | -61% | 1 | 1 | 0% | 629 | 2,748 | +337% | 0 | 0 | — |
case-20 | pass→pass | 14,965 | 10,275 | -31% | 1 | 1 | 0% | 3,090 | 4,596 | +49% | 0 | 0 | — |
case-21 | pass→pass | 13,024 | 9,961 | -24% | 1 | 1 | 0% | 2,429 | 4,355 | +79% | 0 | 0 | — |
case-22 | pass→pass | 12,788 | 8,295 | -35% | 1 | 1 | 0% | 2,666 | 4,287 | +61% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 22 cases were attempted. The headline lift of +18 percentage points is the difference between those two pass rates over the 22 comparable cases.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.