diff --git a/cmd/helm/helm.go b/cmd/helm/helm.go
index 4bfc0d1dd06e6aa6a9d50d780a856cd76f72533c..4eee67957913997c888a5ba8b2193401461b0e92 100644
--- a/cmd/helm/helm.go
+++ b/cmd/helm/helm.go
@@ -136,12 +136,16 @@ func newRootCmd(out io.Writer) *cobra.Command {
 		addFlagsTLS(newStatusCmd(nil, out)),
 		addFlagsTLS(newUpgradeCmd(nil, out)),
 
+		addFlagsTLS(newReleaseTestCmd(nil, out)),
+		addFlagsTLS(newResetCmd(nil, out)),
+		addFlagsTLS(newVersionCmd(nil, out)),
 		newCompletionCmd(out),
 		newHomeCmd(out),
 		newInitCmd(out),
-		addFlagsTLS(newResetCmd(nil, out)),
-		addFlagsTLS(newVersionCmd(nil, out)),
-		addFlagsTLS(newReleaseTestCmd(nil, out)),
+		newResetCmd(nil, out),
+		newVersionCmd(nil, out),
+		newReleaseTestCmd(nil, out),
+		newPluginCmd(out),
 
 		// Hidden documentation generator command: 'helm docs'
 		newDocsCmd(out),
diff --git a/cmd/helm/plugins.go b/cmd/helm/load_plugins.go
similarity index 96%
rename from cmd/helm/plugins.go
rename to cmd/helm/load_plugins.go
index ba1df5565e66dc0964cd0e88772700b132518139..f33161b31bc6a6727a7756433373aa59715547e9 100644
--- a/cmd/helm/plugins.go
+++ b/cmd/helm/load_plugins.go
@@ -31,6 +31,13 @@ import (
 
 const pluginEnvVar = "HELM_PLUGIN"
 
+func pluginDirs(home helmpath.Home) string {
+	if dirs := os.Getenv(pluginEnvVar); dirs != "" {
+		return dirs
+	}
+	return home.Plugins()
+}
+
 // loadPlugins loads plugins into the command list.
 //
 // This follows a different pattern than the other commands because it has
@@ -43,11 +50,7 @@ func loadPlugins(baseCmd *cobra.Command, home helmpath.Home, out io.Writer) {
 		return
 	}
 
-	plugdirs := os.Getenv(pluginEnvVar)
-	if plugdirs == "" {
-		plugdirs = home.Plugins()
-	}
-
+	plugdirs := pluginDirs(home)
 	found, err := findPlugins(plugdirs)
 	if err != nil {
 		fmt.Fprintf(os.Stderr, "failed to load plugins: %s", err)
diff --git a/cmd/helm/plugin.go b/cmd/helm/plugin.go
new file mode 100644
index 0000000000000000000000000000000000000000..58290eec3b94295b76074a248f307b3c7677e07f
--- /dev/null
+++ b/cmd/helm/plugin.go
@@ -0,0 +1,72 @@
+/*
+Copyright 2016 The Kubernetes Authors All rights reserved.
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package main
+
+import (
+	"fmt"
+	"io"
+	"os"
+	"os/exec"
+
+	"k8s.io/helm/pkg/helm/helmpath"
+	"k8s.io/helm/pkg/plugin"
+
+	"github.com/spf13/cobra"
+)
+
+const pluginHelp = `
+Manage client-side Helm plugins.
+`
+
+func newPluginCmd(out io.Writer) *cobra.Command {
+	cmd := &cobra.Command{
+		Use:   "plugin",
+		Short: "add, list, or remove Helm plugins",
+		Long:  pluginHelp,
+	}
+	cmd.AddCommand(
+		newPluginInstallCmd(out),
+		newPluginListCmd(out),
+		newPluginRemoveCmd(out),
+	)
+	return cmd
+}
+
+// runHook will execute a plugin hook.
+func runHook(p *plugin.Plugin, event string, home helmpath.Home) error {
+	hook := p.Metadata.Hooks.Get(event)
+	if hook == "" {
+		return nil
+	}
+
+	prog := exec.Command("sh", "-c", hook)
+	// TODO make this work on windows
+	// I think its ... ВЇ\_(гѓ„)_/ВЇ
+	// prog := exec.Command("cmd", "/C", p.Metadata.Hooks.Install())
+
+	debug("running %s hook: %s", event, prog)
+
+	setupEnv(p.Metadata.Name, p.Dir, home.Plugins(), home)
+	prog.Stdout, prog.Stderr = os.Stdout, os.Stderr
+	if err := prog.Run(); err != nil {
+		if eerr, ok := err.(*exec.ExitError); ok {
+			os.Stderr.Write(eerr.Stderr)
+			return fmt.Errorf("plugin %s hook for %q exited with error", event, p.Metadata.Name)
+		}
+		return err
+	}
+	return nil
+}
diff --git a/cmd/helm/plugin_install.go b/cmd/helm/plugin_install.go
new file mode 100644
index 0000000000000000000000000000000000000000..befa1d8fda656c416e3ad48c7476cbd93fa1ce2e
--- /dev/null
+++ b/cmd/helm/plugin_install.go
@@ -0,0 +1,84 @@
+/*
+Copyright 2016 The Kubernetes Authors All rights reserved.
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package main
+
+import (
+	"fmt"
+	"io"
+
+	"k8s.io/helm/pkg/helm/helmpath"
+	"k8s.io/helm/pkg/plugin"
+	"k8s.io/helm/pkg/plugin/installer"
+
+	"github.com/spf13/cobra"
+)
+
+type pluginInstallCmd struct {
+	source  string
+	version string
+	home    helmpath.Home
+	out     io.Writer
+}
+
+func newPluginInstallCmd(out io.Writer) *cobra.Command {
+	pcmd := &pluginInstallCmd{out: out}
+	cmd := &cobra.Command{
+		Use:   "install [options] <path|url>...",
+		Short: "install one or more Helm plugins",
+		PreRunE: func(cmd *cobra.Command, args []string) error {
+			return pcmd.complete(args)
+		},
+		RunE: func(cmd *cobra.Command, args []string) error {
+			return pcmd.run()
+		},
+	}
+	cmd.Flags().StringVar(&pcmd.version, "version", "", "specify a version constraint. If this is not specified, the latest version is installed")
+	return cmd
+}
+
+func (pcmd *pluginInstallCmd) complete(args []string) error {
+	if err := checkArgsLength(len(args), "plugin"); err != nil {
+		return err
+	}
+	pcmd.source = args[0]
+	pcmd.home = helmpath.Home(homePath())
+	return nil
+}
+
+func (pcmd *pluginInstallCmd) run() error {
+	installer.Debug = flagDebug
+
+	i, err := installer.NewForSource(pcmd.source, pcmd.version, pcmd.home)
+	if err != nil {
+		return err
+	}
+	if err := installer.Install(i); err != nil {
+		return err
+	}
+
+	debug("loading plugin from %s", i.Path())
+	p, err := plugin.LoadDir(i.Path())
+	if err != nil {
+		return err
+	}
+
+	if err := runHook(p, plugin.Install, pcmd.home); err != nil {
+		return err
+	}
+
+	fmt.Fprintf(pcmd.out, "Installed plugin: %s\n", p.Metadata.Name)
+	return nil
+}
diff --git a/cmd/helm/plugin_list.go b/cmd/helm/plugin_list.go
new file mode 100644
index 0000000000000000000000000000000000000000..e71b720387119f4349d24d405aa7150db8de1642
--- /dev/null
+++ b/cmd/helm/plugin_list.go
@@ -0,0 +1,63 @@
+/*
+Copyright 2016 The Kubernetes Authors All rights reserved.
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package main
+
+import (
+	"fmt"
+	"io"
+
+	"k8s.io/helm/pkg/helm/helmpath"
+
+	"github.com/gosuri/uitable"
+	"github.com/spf13/cobra"
+)
+
+type pluginListCmd struct {
+	home helmpath.Home
+	out  io.Writer
+}
+
+func newPluginListCmd(out io.Writer) *cobra.Command {
+	pcmd := &pluginListCmd{out: out}
+	cmd := &cobra.Command{
+		Use:   "list",
+		Short: "list installed Helm plugins",
+		RunE: func(cmd *cobra.Command, args []string) error {
+			pcmd.home = helmpath.Home(homePath())
+			return pcmd.run()
+		},
+	}
+	return cmd
+}
+
+func (pcmd *pluginListCmd) run() error {
+	plugdirs := pluginDirs(pcmd.home)
+
+	debug("pluginDirs: %s", plugdirs)
+
+	plugins, err := findPlugins(plugdirs)
+	if err != nil {
+		return err
+	}
+
+	table := uitable.New()
+	table.AddRow("NAME", "VERSION", "DESCRIPTION")
+	for _, p := range plugins {
+		table.AddRow(p.Metadata.Name, p.Metadata.Version, p.Metadata.Description)
+	}
+	fmt.Fprintln(pcmd.out, table)
+	return nil
+}
diff --git a/cmd/helm/plugin_remove.go b/cmd/helm/plugin_remove.go
new file mode 100644
index 0000000000000000000000000000000000000000..74fc680b71132f12c25523721ad1073b346b10ff
--- /dev/null
+++ b/cmd/helm/plugin_remove.go
@@ -0,0 +1,92 @@
+/*
+Copyright 2016 The Kubernetes Authors All rights reserved.
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package main
+
+import (
+	"fmt"
+	"io"
+	"os"
+
+	"k8s.io/helm/pkg/helm/helmpath"
+	"k8s.io/helm/pkg/plugin"
+
+	"github.com/spf13/cobra"
+)
+
+type pluginRemoveCmd struct {
+	names []string
+	home  helmpath.Home
+	out   io.Writer
+}
+
+func newPluginRemoveCmd(out io.Writer) *cobra.Command {
+	pcmd := &pluginRemoveCmd{out: out}
+	cmd := &cobra.Command{
+		Use:   "remove <plugin>...",
+		Short: "remove one or more Helm plugins",
+		PreRunE: func(cmd *cobra.Command, args []string) error {
+			return pcmd.complete(args)
+		},
+		RunE: func(cmd *cobra.Command, args []string) error {
+			return pcmd.run()
+		},
+	}
+	return cmd
+}
+
+func (pcmd *pluginRemoveCmd) complete(args []string) error {
+	if err := checkArgsLength(len(args), "plugin"); err != nil {
+		return err
+	}
+	pcmd.names = args
+	pcmd.home = helmpath.Home(homePath())
+	return nil
+}
+
+func (pcmd *pluginRemoveCmd) run() error {
+	plugdirs := pluginDirs(pcmd.home)
+	debug("loading installed plugins from %s", plugdirs)
+	plugins, err := findPlugins(plugdirs)
+	if err != nil {
+		return err
+	}
+
+	for _, name := range pcmd.names {
+		if found := findPlugin(plugins, name); found != nil {
+			if err := removePlugin(found, pcmd.home); err != nil {
+				return err
+			}
+			fmt.Fprintf(pcmd.out, "Removed plugin: %s\n", name)
+		}
+	}
+	return nil
+}
+
+func removePlugin(p *plugin.Plugin, home helmpath.Home) error {
+	if err := os.Remove(p.Dir); err != nil {
+		return err
+	}
+	return runHook(p, plugin.Delete, home)
+}
+
+func findPlugin(plugins []*plugin.Plugin, name string) *plugin.Plugin {
+	for _, p := range plugins {
+		if p.Metadata.Name == name {
+			return p
+		}
+	}
+	return nil
+}
diff --git a/cmd/helm/plugins_test.go b/cmd/helm/plugin_test.go
similarity index 100%
rename from cmd/helm/plugins_test.go
rename to cmd/helm/plugin_test.go
diff --git a/cmd/helm/printer.go b/cmd/helm/printer.go
index 34b1122beffd9bfd8f8c8a460e7784bbb8857bdb..dbffd5c44a334c16484a01b0d05fa9e963b02edf 100644
--- a/cmd/helm/printer.go
+++ b/cmd/helm/printer.go
@@ -17,6 +17,7 @@ limitations under the License.
 package main
 
 import (
+	"fmt"
 	"io"
 	"text/template"
 	"time"
@@ -72,3 +73,10 @@ func tpl(t string, vals map[string]interface{}, out io.Writer) error {
 	}
 	return tt.Execute(out, vals)
 }
+
+func debug(format string, args ...interface{}) {
+	if flagDebug {
+		format = fmt.Sprintf("[debug] %s\n", format)
+		fmt.Printf(format, args...)
+	}
+}
diff --git a/glide.lock b/glide.lock
index 3ea2cc6178df73dc8eb20060804f0c6795b60e86..522bbc0160fa4d1ea37048afc0d2fb0c3dc743bd 100644
--- a/glide.lock
+++ b/glide.lock
@@ -1,5 +1,5 @@
-hash: df0fa621e6a6f80dbfeb815d9d8aa308c50346a9821e401b19b6f10782da3774
-updated: 2017-04-03T17:00:07.670429885-06:00
+hash: 6a39d319e98b1b4305c48e9b718604b723184f27a1366efcedc42d95bcbeb0c8
+updated: 2017-04-06T10:04:41.822904395-07:00
 imports:
 - name: cloud.google.com/go
   version: 3b1ae45394a234c385be014e9a488f2bb6eef821
@@ -185,6 +185,8 @@ imports:
   version: 3f0ab6d4ab4bed1c61caf056b63a6e62190c7801
 - name: github.com/Masterminds/sprig
   version: 23597e5f6ad0e4d590e71314bfd0251a4a3cf849
+- name: github.com/Masterminds/vcs
+  version: 795e20f901c3d561de52811fb3488a2cb2c8588b
 - name: github.com/mattn/go-runewidth
   version: d6bea18f789704b5f83375793155289da36a3c7f
 - name: github.com/mitchellh/go-wordwrap
@@ -300,7 +302,7 @@ imports:
 - name: gopkg.in/yaml.v2
   version: a83829b6f1293c91addabc89d0571c246397bbf4
 - name: k8s.io/kubernetes
-  version: ea8f6637b639246faa14a8d5c6f864100fcb77a9
+  version: 114f8911f9597be669a747ab72787e0bd74c9359
   subpackages:
   - cmd/kubeadm/app/apis/kubeadm
   - cmd/kubeadm/app/apis/kubeadm/install
diff --git a/glide.yaml b/glide.yaml
index a703bff5b9f28c1561c8ebace08d60ab7da6e62f..7ebd3b7aee8a5ebda06bfa49262e75ff66639b8e 100644
--- a/glide.yaml
+++ b/glide.yaml
@@ -8,6 +8,8 @@ import:
   version: f62e98d28ab7ad31d707ba837a966378465c7b57
 - package: github.com/spf13/pflag
   version: 5ccb023bc27df288a957c5e994cd44fd19619465
+- package: github.com/Masterminds/vcs
+  version: ~1.11.0
 - package: github.com/Masterminds/sprig
   version: ^2.10
 - package: github.com/ghodss/yaml
diff --git a/pkg/plugin/cache/cache.go b/pkg/plugin/cache/cache.go
new file mode 100644
index 0000000000000000000000000000000000000000..a1d3224c8b48959a88fc4eff6696ba45fc42048b
--- /dev/null
+++ b/pkg/plugin/cache/cache.go
@@ -0,0 +1,74 @@
+/*
+Copyright 2016 The Kubernetes Authors All rights reserved.
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+// Package cache provides a key generator for vcs urls.
+package cache // import "k8s.io/helm/pkg/plugin/cache"
+
+import (
+	"net/url"
+	"regexp"
+	"strings"
+)
+
+// Thanks glide!
+
+// scpSyntaxRe matches the SCP-like addresses used to access repos over SSH.
+var scpSyntaxRe = regexp.MustCompile(`^([a-zA-Z0-9_]+)@([a-zA-Z0-9._-]+):(.*)$`)
+
+// Key generates a cache key based on a url or scp string. The key is file
+// system safe.
+func Key(repo string) (string, error) {
+
+	var u *url.URL
+	var err error
+	var strip bool
+	if m := scpSyntaxRe.FindStringSubmatch(repo); m != nil {
+		// Match SCP-like syntax and convert it to a URL.
+		// Eg, "git@github.com:user/repo" becomes
+		// "ssh://git@github.com/user/repo".
+		u = &url.URL{
+			Scheme: "ssh",
+			User:   url.User(m[1]),
+			Host:   m[2],
+			Path:   "/" + m[3],
+		}
+		strip = true
+	} else {
+		u, err = url.Parse(repo)
+		if err != nil {
+			return "", err
+		}
+	}
+
+	if strip {
+		u.Scheme = ""
+	}
+
+	var key string
+	if u.Scheme != "" {
+		key = u.Scheme + "-"
+	}
+	if u.User != nil && u.User.Username() != "" {
+		key = key + u.User.Username() + "-"
+	}
+	key = key + u.Host
+	if u.Path != "" {
+		key = key + strings.Replace(u.Path, "/", "-", -1)
+	}
+
+	key = strings.Replace(key, ":", "-", -1)
+
+	return key, nil
+}
diff --git a/pkg/plugin/hooks.go b/pkg/plugin/hooks.go
new file mode 100644
index 0000000000000000000000000000000000000000..1f435f9f8587bc0cbd270d4f83834630ae06e6a8
--- /dev/null
+++ b/pkg/plugin/hooks.go
@@ -0,0 +1,33 @@
+/*
+Copyright 2016 The Kubernetes Authors All rights reserved.
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package plugin // import "k8s.io/helm/pkg/plugin"
+
+// Types of hooks
+const (
+	// Install is executed after the plugin is added.
+	Install = "install"
+	// Delete is executed after the plugin is removed.
+	Delete = "delete"
+)
+
+// Hooks is a map of events to commands.
+type Hooks map[string]string
+
+// Get returns a hook for an event.
+func (hooks Hooks) Get(event string) string {
+	h, _ := hooks[event]
+	return h
+}
diff --git a/pkg/plugin/installer/base.go b/pkg/plugin/installer/base.go
new file mode 100644
index 0000000000000000000000000000000000000000..0664dae763e4effd11f16772ac59f884f33a16e9
--- /dev/null
+++ b/pkg/plugin/installer/base.go
@@ -0,0 +1,48 @@
+/*
+Copyright 2016 The Kubernetes Authors All rights reserved.
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package installer // import "k8s.io/helm/pkg/plugin/installer"
+
+import (
+	"os"
+	"path/filepath"
+
+	"k8s.io/helm/pkg/helm/helmpath"
+)
+
+type base struct {
+	// Source is the reference to a plugin
+	Source string
+	// HelmHome is the $HELM_HOME directory
+	HelmHome helmpath.Home
+}
+
+func newBase(source string, home helmpath.Home) base {
+	return base{source, home}
+}
+
+// link creates a symlink from the plugin source to $HELM_HOME.
+func (b *base) link(from string) error {
+	debug("symlinking %s to %s", from, b.Path())
+	return os.Symlink(from, b.Path())
+}
+
+// Path is where the plugin will be symlinked to.
+func (b *base) Path() string {
+	if b.Source == "" {
+		return ""
+	}
+	return filepath.Join(b.HelmHome.Plugins(), filepath.Base(b.Source))
+}
diff --git a/pkg/plugin/installer/doc.go b/pkg/plugin/installer/doc.go
new file mode 100644
index 0000000000000000000000000000000000000000..a2a66f3e1dcd1535ed1146e11ffd19268f84800b
--- /dev/null
+++ b/pkg/plugin/installer/doc.go
@@ -0,0 +1,17 @@
+/*
+Copyright 2016 The Kubernetes Authors All rights reserved.
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+// Package installer provides an interface for installing Helm plugins.
+package installer // import "k8s.io/helm/pkg/plugin/installer"
diff --git a/pkg/plugin/installer/installer.go b/pkg/plugin/installer/installer.go
new file mode 100644
index 0000000000000000000000000000000000000000..31ef9ae534066ac42308a74e54cb9cc57ecac955
--- /dev/null
+++ b/pkg/plugin/installer/installer.go
@@ -0,0 +1,72 @@
+/*
+Copyright 2016 The Kubernetes Authors All rights reserved.
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package installer
+
+import (
+	"errors"
+	"fmt"
+	"os"
+	"path/filepath"
+
+	"k8s.io/helm/pkg/helm/helmpath"
+)
+
+// ErrMissingMetadata indicates that plugin.yaml is missing.
+var ErrMissingMetadata = errors.New("plugin metadata (plugin.yaml) missing")
+
+// Debug enables verbose output.
+var Debug bool
+
+// Installer provides an interface for installing helm client plugins.
+type Installer interface {
+	// Install adds a plugin to $HELM_HOME.
+	Install() error
+	// Path is the directory of the installed plugin.
+	Path() string
+}
+
+// Install installs a plugin to $HELM_HOME.
+func Install(i Installer) error {
+	return i.Install()
+}
+
+// NewForSource determines the correct Installer for the given source.
+func NewForSource(source, version string, home helmpath.Home) (Installer, error) {
+	// Check if source is a local directory
+	if isLocalReference(source) {
+		return NewLocalInstaller(source, home)
+	}
+	return NewVCSInstaller(source, version, home)
+}
+
+// isLocalReference checks if the source exists on the filesystem.
+func isLocalReference(source string) bool {
+	_, err := os.Stat(source)
+	return err == nil
+}
+
+// isPlugin checks if the directory contains a plugin.yaml file.
+func isPlugin(dirname string) bool {
+	_, err := os.Stat(filepath.Join(dirname, "plugin.yaml"))
+	return err == nil
+}
+
+func debug(format string, args ...interface{}) {
+	if Debug {
+		format = fmt.Sprintf("[debug] %s\n", format)
+		fmt.Printf(format, args...)
+	}
+}
diff --git a/pkg/plugin/installer/local_installer.go b/pkg/plugin/installer/local_installer.go
new file mode 100644
index 0000000000000000000000000000000000000000..7ab588d60fa7b515cdf63df77c9a43fd51b86907
--- /dev/null
+++ b/pkg/plugin/installer/local_installer.go
@@ -0,0 +1,49 @@
+/*
+Copyright 2016 The Kubernetes Authors All rights reserved.
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package installer // import "k8s.io/helm/pkg/plugin/installer"
+
+import (
+	"path/filepath"
+
+	"k8s.io/helm/pkg/helm/helmpath"
+)
+
+// LocalInstaller installs plugins from the filesystem.
+type LocalInstaller struct {
+	base
+}
+
+// NewLocalInstaller creates a new LocalInstaller.
+func NewLocalInstaller(source string, home helmpath.Home) (*LocalInstaller, error) {
+	i := &LocalInstaller{
+		base: newBase(source, home),
+	}
+	return i, nil
+}
+
+// Install creates a symlink to the plugin directory in $HELM_HOME.
+//
+// Implements Installer.
+func (i *LocalInstaller) Install() error {
+	if !isPlugin(i.Source) {
+		return ErrMissingMetadata
+	}
+	src, err := filepath.Abs(i.Source)
+	if err != nil {
+		return err
+	}
+	return i.link(src)
+}
diff --git a/pkg/plugin/installer/local_installer_test.go b/pkg/plugin/installer/local_installer_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..a2a1b541c1f0129665c2d5223e63628a6198eeab
--- /dev/null
+++ b/pkg/plugin/installer/local_installer_test.go
@@ -0,0 +1,64 @@
+/*
+Copyright 2016 The Kubernetes Authors All rights reserved.
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package installer // import "k8s.io/helm/pkg/plugin/installer"
+
+import (
+	"io/ioutil"
+	"os"
+	"path/filepath"
+	"testing"
+
+	"k8s.io/helm/pkg/helm/helmpath"
+)
+
+var _ Installer = new(LocalInstaller)
+
+func TestLocalInstaller(t *testing.T) {
+	hh, err := ioutil.TempDir("", "helm-home-")
+	if err != nil {
+		t.Fatal(err)
+	}
+	defer os.Remove(hh)
+
+	home := helmpath.Home(hh)
+	if err := os.MkdirAll(home.Plugins(), 0755); err != nil {
+		t.Fatalf("Could not create %s: %s", home.Plugins(), err)
+	}
+
+	// Make a temp dir
+	tdir, err := ioutil.TempDir("", "helm-installer-")
+	if err != nil {
+		t.Fatal(err)
+	}
+	defer os.Remove(tdir)
+	if err := ioutil.WriteFile(filepath.Join(tdir, "plugin.yaml"), []byte{}, 0644); err != nil {
+		t.Fatal(err)
+	}
+
+	source := "../testdata/plugdir/echo"
+	i, err := NewForSource(source, "", home)
+	if err != nil {
+		t.Errorf("unexpected error: %s", err)
+	}
+
+	if err := Install(i); err != nil {
+		t.Error(err)
+	}
+
+	if i.Path() != home.Path("plugins", "echo") {
+		t.Errorf("expected path '$HELM_HOME/plugins/helm-env', got %q", i.Path())
+	}
+}
diff --git a/pkg/plugin/installer/vcs_installer.go b/pkg/plugin/installer/vcs_installer.go
new file mode 100644
index 0000000000000000000000000000000000000000..537aca3e6fcee9db94ffd3c19ada0ee307fe6f2c
--- /dev/null
+++ b/pkg/plugin/installer/vcs_installer.go
@@ -0,0 +1,145 @@
+/*
+Copyright 2016 The Kubernetes Authors All rights reserved.
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package installer // import "k8s.io/helm/pkg/plugin/installer"
+
+import (
+	"os"
+	"sort"
+
+	"github.com/Masterminds/semver"
+	"github.com/Masterminds/vcs"
+
+	"k8s.io/helm/pkg/helm/helmpath"
+	"k8s.io/helm/pkg/plugin/cache"
+)
+
+// VCSInstaller installs plugins from remote a repository.
+type VCSInstaller struct {
+	Repo    vcs.Repo
+	Version string
+	base
+}
+
+// NewVCSInstaller creates a new VCSInstaller.
+func NewVCSInstaller(source, version string, home helmpath.Home) (*VCSInstaller, error) {
+	key, err := cache.Key(source)
+	if err != nil {
+		return nil, err
+	}
+	cachedpath := home.Path("cache", "plugins", key)
+	repo, err := vcs.NewRepo(source, cachedpath)
+	if err != nil {
+		return nil, err
+	}
+	i := &VCSInstaller{
+		Repo:    repo,
+		Version: version,
+		base:    newBase(source, home),
+	}
+	return i, err
+}
+
+// Install clones a remote repository and creates a symlink to the plugin directory in HELM_HOME.
+//
+// Implements Installer.
+func (i *VCSInstaller) Install() error {
+	if err := i.sync(i.Repo); err != nil {
+		return err
+	}
+
+	ref, err := i.solveVersion(i.Repo)
+	if err != nil {
+		return err
+	}
+
+	if err := i.setVersion(i.Repo, ref); err != nil {
+		return err
+	}
+
+	if !isPlugin(i.Repo.LocalPath()) {
+		return ErrMissingMetadata
+	}
+
+	return i.link(i.Repo.LocalPath())
+}
+
+func (i *VCSInstaller) solveVersion(repo vcs.Repo) (string, error) {
+	if i.Version == "" {
+		return "", nil
+	}
+
+	if repo.IsReference(i.Version) {
+		return i.Version, nil
+	}
+
+	// Create the constraint first to make sure it's valid before
+	// working on the repo.
+	constraint, err := semver.NewConstraint(i.Version)
+	if err != nil {
+		return "", err
+	}
+
+	// Get the tags and branches (in that order)
+	refs, err := repo.Tags()
+	if err != nil {
+		return "", err
+	}
+	debug("found refs: %s", refs)
+
+	// Convert and filter the list to semver.Version instances
+	semvers := getSemVers(refs)
+
+	// Sort semver list
+	sort.Sort(sort.Reverse(semver.Collection(semvers)))
+	for _, v := range semvers {
+		if constraint.Check(v) {
+			// If the constrint passes get the original reference
+			ver := v.Original()
+			debug("setting to %s", ver)
+			return ver, nil
+		}
+	}
+	return "", nil
+}
+
+// setVersion attempts to checkout the version
+func (i *VCSInstaller) setVersion(repo vcs.Repo, ref string) error {
+	debug("setting version to %q", i.Version)
+	return repo.UpdateVersion(ref)
+}
+
+// sync will clone or update a remote repo.
+func (i *VCSInstaller) sync(repo vcs.Repo) error {
+	if _, err := os.Stat(repo.LocalPath()); os.IsNotExist(err) {
+		debug("cloning %s to %s", repo.Remote(), repo.LocalPath())
+		return repo.Get()
+	}
+	debug("updating %s", repo.Remote())
+	return repo.Update()
+}
+
+// Filter a list of versions to only included semantic versions. The response
+// is a mapping of the original version to the semantic version.
+func getSemVers(refs []string) []*semver.Version {
+	var sv []*semver.Version
+	for _, r := range refs {
+		v, err := semver.NewVersion(r)
+		if err == nil {
+			sv = append(sv, v)
+		}
+	}
+	return sv
+}
diff --git a/pkg/plugin/installer/vcs_installer_test.go b/pkg/plugin/installer/vcs_installer_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..e340bbe5a99393f0b729ba50f1b8eeec132485f4
--- /dev/null
+++ b/pkg/plugin/installer/vcs_installer_test.go
@@ -0,0 +1,90 @@
+/*
+Copyright 2016 The Kubernetes Authors All rights reserved.
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package installer // import "k8s.io/helm/pkg/plugin/installer"
+
+import (
+	"io/ioutil"
+	"os"
+	"testing"
+
+	"k8s.io/helm/pkg/helm/helmpath"
+
+	"github.com/Masterminds/vcs"
+)
+
+var _ Installer = new(VCSInstaller)
+
+type testRepo struct {
+	local, remote, current string
+	tags, branches         []string
+	err                    error
+	vcs.Repo
+}
+
+func (r *testRepo) LocalPath() string           { return r.local }
+func (r *testRepo) Remote() string              { return r.remote }
+func (r *testRepo) Update() error               { return r.err }
+func (r *testRepo) Get() error                  { return r.err }
+func (r *testRepo) IsReference(string) bool     { return false }
+func (r *testRepo) Tags() ([]string, error)     { return r.tags, r.err }
+func (r *testRepo) Branches() ([]string, error) { return r.branches, r.err }
+func (r *testRepo) UpdateVersion(version string) error {
+	r.current = version
+	return r.err
+}
+
+func TestVCSInstaller(t *testing.T) {
+	hh, err := ioutil.TempDir("", "helm-home-")
+	if err != nil {
+		t.Fatal(err)
+	}
+	defer os.Remove(hh)
+
+	home := helmpath.Home(hh)
+	if err := os.MkdirAll(home.Plugins(), 0755); err != nil {
+		t.Fatalf("Could not create %s: %s", home.Plugins(), err)
+	}
+
+	source := "https://github.com/adamreese/helm-env"
+	repo := &testRepo{
+		local: "../testdata/plugdir/echo",
+		tags:  []string{"0.1.0", "0.1.1"},
+	}
+
+	i, err := NewForSource(source, "~0.1.0", home)
+	if err != nil {
+		t.Errorf("unexpected error: %s", err)
+	}
+
+	// ensure a VCSInstaller was returned
+	vcsInstaller, ok := i.(*VCSInstaller)
+	if !ok {
+		t.Error("expected a VCSInstaller")
+	}
+
+	// set the testRepo in the VCSInstaller
+	vcsInstaller.Repo = repo
+
+	if err := Install(i); err != nil {
+		t.Error(err)
+	}
+	if repo.current != "0.1.1" {
+		t.Errorf("expected version '0.1.1', got %q", repo.current)
+	}
+	if i.Path() != home.Path("plugins", "helm-env") {
+		t.Errorf("expected path '$HELM_HOME/plugins/helm-env', got %q", i.Path())
+	}
+}
diff --git a/pkg/plugin/plugin.go b/pkg/plugin/plugin.go
index bc8cbb73283f27757c9c5709c62e7950af17066f..d9fbf80ea03e75b6953a8d748a3d3e565aad0ff6 100644
--- a/pkg/plugin/plugin.go
+++ b/pkg/plugin/plugin.go
@@ -13,7 +13,7 @@ See the License for the specific language governing permissions and
 limitations under the License.
 */
 
-package plugin
+package plugin // import "k8s.io/helm/pkg/plugin"
 
 import (
 	"io/ioutil"
@@ -21,6 +21,7 @@ import (
 	"path/filepath"
 	"strings"
 
+	"github.com/Masterminds/vcs"
 	"github.com/ghodss/yaml"
 )
 
@@ -64,6 +65,9 @@ type Metadata struct {
 	// Setting this will cause a number of side effects, such as the
 	// automatic setting of HELM_HOST.
 	UseTunnel bool `json:"useTunnel"`
+
+	// Hooks are commands that will run on events.
+	Hooks Hooks
 }
 
 // Plugin represents a plugin.
@@ -72,6 +76,17 @@ type Plugin struct {
 	Metadata *Metadata
 	// Dir is the string path to the directory that holds the plugin.
 	Dir string
+	// Remote is the remote repo location.
+	Remote string
+}
+
+func detectSource(dirname string) (string, error) {
+	if repo, err := vcs.NewRepo("", dirname); err == nil {
+		if repo.CheckLocal() {
+			return repo.Remote(), nil
+		}
+	}
+	return os.Readlink(dirname)
 }
 
 // PrepareCommand takes a Plugin.Command and prepares it for execution.
@@ -101,6 +116,10 @@ func LoadDir(dirname string) (*Plugin, error) {
 	}
 
 	plug := &Plugin{Dir: dirname}
+	if src, err := detectSource(dirname); err == nil {
+		plug.Remote = src
+	}
+
 	if err := yaml.Unmarshal(data, &plug.Metadata); err != nil {
 		return nil, err
 	}
diff --git a/pkg/plugin/plugin_test.go b/pkg/plugin/plugin_test.go
index afcf4b9b3de951aa58e9a9def0a69ee3f0e81f21..e0abb91073148d48da4140ee50375279d2b57835 100644
--- a/pkg/plugin/plugin_test.go
+++ b/pkg/plugin/plugin_test.go
@@ -13,7 +13,7 @@ See the License for the specific language governing permissions and
 limitations under the License.
 */
 
-package plugin
+package plugin // import "k8s.io/helm/pkg/plugin"
 
 import (
 	"reflect"
@@ -82,6 +82,9 @@ func TestLoadDir(t *testing.T) {
 		Command:     "$HELM_PLUGIN_SELF/hello.sh",
 		UseTunnel:   true,
 		IgnoreFlags: true,
+		Hooks: map[string]string{
+			Install: "echo installing...",
+		},
 	}
 
 	if !reflect.DeepEqual(expect, plug.Metadata) {
diff --git a/pkg/plugin/testdata/plugdir/echo/plugin.yaml b/pkg/plugin/testdata/plugdir/echo/plugin.yaml
index da6f656ebbc3d732fa1b4963b68de7a1f28722e8..8baa35b6d5f2b5478c60e2858d60fc056c0f35ab 100644
--- a/pkg/plugin/testdata/plugdir/echo/plugin.yaml
+++ b/pkg/plugin/testdata/plugdir/echo/plugin.yaml
@@ -4,3 +4,5 @@ usage: "echo something"
 description: |-
   This is a testing fixture.
 command: "echo Hello"
+hooks:
+  install: "echo Installing"
diff --git a/pkg/plugin/testdata/plugdir/hello/plugin.yaml b/pkg/plugin/testdata/plugdir/hello/plugin.yaml
index 94b7f43dccf499fd187bd78976f00032d01f720a..cdb27b29122cbf93dd243137bddff23c5e55f565 100644
--- a/pkg/plugin/testdata/plugdir/hello/plugin.yaml
+++ b/pkg/plugin/testdata/plugdir/hello/plugin.yaml
@@ -6,3 +6,6 @@ description: |-
 command: "$HELM_PLUGIN_SELF/hello.sh"
 useTunnel: true
 ignoreFlags: true
+install: "echo installing..."
+hooks:
+  install: "echo installing..."