diff --git a/Makefile b/Makefile
index ea646f520885aabca2c46aa8fe556971193be41d..8612b0fa7b5c34f9937b6700ff4e3b8b0f33675f 100644
--- a/Makefile
+++ b/Makefile
@@ -22,7 +22,7 @@ GOFMT = gofmt
 
 JSONNET_FILES = lib/kubecfg_test.jsonnet examples/guestbook.jsonnet
 # TODO: Simplify this once ./... ignores ./vendor
-GO_PACKAGES = ./cmd/... ./utils/... ./pkg/...
+GO_PACKAGES = ./cmd/... ./utils/... ./pkg/... ./metadata/...
 
 all: kubecfg
 
diff --git a/metadata/clusterspec.go b/metadata/clusterspec.go
new file mode 100644
index 0000000000000000000000000000000000000000..bbe44dd15bdc584da78dcd062f2cc11441ee948e
--- /dev/null
+++ b/metadata/clusterspec.go
@@ -0,0 +1,43 @@
+package metadata
+
+import (
+	"github.com/spf13/afero"
+)
+
+type clusterSpecFile struct {
+	specPath AbsPath
+}
+
+func (cs *clusterSpecFile) data() ([]byte, error) {
+	return afero.ReadFile(appFS, string(cs.specPath))
+}
+
+func (cs *clusterSpecFile) resource() string {
+	return string(cs.specPath)
+}
+
+type clusterSpecLive struct {
+	apiServerURL string
+}
+
+func (cs *clusterSpecLive) data() ([]byte, error) {
+	// TODO: Implement getting spec from path, k8sVersion, and URL.
+	panic("Not implemented")
+}
+
+func (cs *clusterSpecLive) resource() string {
+	return string(cs.apiServerURL)
+}
+
+type clusterSpecVersion struct {
+	k8sVersion string
+}
+
+func (cs *clusterSpecVersion) data() ([]byte, error) {
+	// TODO: Implement getting spec from path, k8sVersion, and URL.
+	panic("Not implemented")
+}
+
+func (cs *clusterSpecVersion) resource() string {
+	return string(cs.k8sVersion)
+}
diff --git a/metadata/clusterspec_test.go b/metadata/clusterspec_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..4d13beda69ff703293c4cf29e4546c30a51aeccf
--- /dev/null
+++ b/metadata/clusterspec_test.go
@@ -0,0 +1,76 @@
+package metadata
+
+import (
+	"path/filepath"
+	"testing"
+)
+
+type parseSuccess struct {
+	input  string
+	target ClusterSpec
+}
+
+var successTests = []parseSuccess{
+	{"version:v1.7.1", &clusterSpecVersion{"v1.7.1"}},
+	{"file:swagger.json", &clusterSpecFile{"swagger.json"}},
+	{"url:file:///some_file", &clusterSpecLive{"file:///some_file"}},
+}
+
+func TestClusterSpecParsingSuccess(t *testing.T) {
+	for _, test := range successTests {
+		parsed, err := ParseClusterSpec(test.input)
+		if err != nil {
+			t.Errorf("Failed to parse spec: %v", err)
+		}
+
+		parsedResource := parsed.resource()
+		targetResource := test.target.resource()
+
+		switch pt := parsed.(type) {
+		case *clusterSpecLive:
+		case *clusterSpecVersion:
+			if parsedResource != targetResource {
+				t.Errorf("Expected version '%v', got '%v'", parsedResource, targetResource)
+			}
+		case *clusterSpecFile:
+			// Techncially we're cheating here by passing a *relative path*
+			// into `newPathSpec` instead of an absolute one. This is to
+			// make it work on multiple machines. We convert it here, after
+			// the fact.
+			absPath, err := filepath.Abs(targetResource)
+			if err != nil {
+				t.Errorf("Failed to convert `file:` spec to an absolute path: %v", err)
+			}
+
+			if parsedResource != absPath {
+				t.Errorf("Expected path '%v', got '%v'", absPath, parsedResource)
+			}
+		default:
+			t.Errorf("Unknown cluster spec type '%v'", pt)
+		}
+	}
+}
+
+type parseFailure struct {
+	input    string
+	errorMsg string
+}
+
+var failureTests = []parseFailure{
+	{"fakeprefix:foo", "Could not parse cluster spec 'fakeprefix:foo'"},
+	{"foo:", "Invalid API specification 'foo:'"},
+	{"version:", "Invalid API specification 'version:'"},
+	{"file:", "Invalid API specification 'file:'"},
+	{"url:", "Invalid API specification 'url:'"},
+}
+
+func TestClusterSpecParsingFailure(t *testing.T) {
+	for _, test := range failureTests {
+		_, err := ParseClusterSpec(test.input)
+		if err == nil {
+			t.Errorf("Cluster spec parse for '%s' should have failed, but succeeded", test.input)
+		} else if msg := err.Error(); msg != test.errorMsg {
+			t.Errorf("Expected cluster spec parse error: '%s', got: '%s'", test.errorMsg, msg)
+		}
+	}
+}
diff --git a/metadata/interface.go b/metadata/interface.go
new file mode 100644
index 0000000000000000000000000000000000000000..5d9ed9e39458da38ff544adde7b4e9c890df77ba
--- /dev/null
+++ b/metadata/interface.go
@@ -0,0 +1,80 @@
+package metadata
+
+import (
+	"fmt"
+	"path/filepath"
+	"strings"
+
+	"github.com/spf13/afero"
+)
+
+// AbsPath is an advisory type that represents an absolute path. It is advisory
+// in that it is not forced to be absolute, but rather, meant to indicate
+// intent, and make code easier to read.
+type AbsPath string
+
+// Manager abstracts over a ksonnet application's metadata, allowing users to do
+// things like: create and delete environments; search for prototypes; vendor
+// libraries; and other non-core-application tasks.
+type Manager interface {
+	Root() AbsPath
+	//
+	// TODO: Fill in methods as we need them.
+	//
+	// GetPrototype(id string) Protoype
+	// SearchPrototypes(query string) []Protoype
+	// VendorLibrary(uri, version string) error
+	// CreateEnv(name string, spec *ClusterSpec) error
+	// DeleteEnv(name string) error
+	//
+}
+
+// Find will recursively search the current directory and its parents for a
+// `.ksonnet` folder, which marks the application root. Returns error if there
+// is no application root.
+func Find(path AbsPath) (Manager, error) {
+	return findManager(path, afero.NewOsFs())
+}
+
+// Init will retrieve a cluster API specification, generate a
+// capabilities-compliant version of ksonnet-lib, and then generate the
+// directory tree for an application.
+func Init(rootPath AbsPath, spec ClusterSpec) (Manager, error) {
+	return initManager(rootPath, spec, afero.NewOsFs())
+}
+
+// ClusterSpec represents the API supported by some cluster. There are several
+// ways to specify a cluster, including: querying the API server, reading an
+// OpenAPI spec in some file, or consulting the OpenAPI spec released in a
+// specific version of Kubernetes.
+type ClusterSpec interface {
+	data() ([]byte, error)
+	resource() string // For testing parsing logic.
+}
+
+// ParseClusterSpec will parse a cluster spec flag and output a well-formed
+// ClusterSpec object. For example, if the flag is `--version:v1.7.1`, then we
+// will output a ClusterSpec representing the cluster specification associated
+// with the `v1.7.1` build of Kubernetes.
+func ParseClusterSpec(specFlag string) (ClusterSpec, error) {
+	split := strings.SplitN(specFlag, ":", 2)
+	if len(split) == 0 || len(split) == 1 || split[1] == "" {
+		return nil, fmt.Errorf("Invalid API specification '%s'", specFlag)
+	}
+
+	switch split[0] {
+	case "version":
+		return &clusterSpecVersion{k8sVersion: split[1]}, nil
+	case "file":
+		abs, err := filepath.Abs(split[1])
+		if err != nil {
+			return nil, err
+		}
+		absPath := AbsPath(abs)
+		return &clusterSpecFile{specPath: absPath}, nil
+	case "url":
+		return &clusterSpecLive{apiServerURL: split[1]}, nil
+	default:
+		return nil, fmt.Errorf("Could not parse cluster spec '%s'", specFlag)
+	}
+}
diff --git a/metadata/manager.go b/metadata/manager.go
new file mode 100644
index 0000000000000000000000000000000000000000..9898bfed3b3ffbcfec433cbc953632b745270570
--- /dev/null
+++ b/metadata/manager.go
@@ -0,0 +1,138 @@
+package metadata
+
+import (
+	"fmt"
+	"os"
+	"path"
+	"path/filepath"
+
+	"github.com/spf13/afero"
+)
+
+func appendToAbsPath(originalPath AbsPath, toAppend ...string) AbsPath {
+	paths := append([]string{string(originalPath)}, toAppend...)
+	return AbsPath(path.Join(paths...))
+}
+
+const (
+	defaultEnvName = "dev"
+
+	ksonnetDir    = ".ksonnet"
+	libDir        = "lib"
+	componentsDir = "components"
+	vendorDir     = "vendor"
+	schemaDir     = "vendor/schema"
+	vendorLibDir  = "vendor/lib"
+
+	schemaFilename = "swagger.json"
+)
+
+type manager struct {
+	appFS afero.Fs
+
+	rootPath       AbsPath
+	ksonnetPath    AbsPath
+	libPath        AbsPath
+	componentsPath AbsPath
+	vendorDir      AbsPath
+	schemaDir      AbsPath
+	vendorLibDir   AbsPath
+}
+
+func findManager(abs AbsPath, appFS afero.Fs) (*manager, error) {
+	var lastBase string
+	currBase := string(abs)
+
+	for {
+		currPath := path.Join(currBase, ksonnetDir)
+		exists, err := afero.Exists(appFS, currPath)
+		if err != nil {
+			return nil, err
+		}
+		if exists {
+			return newManager(AbsPath(currBase), appFS), nil
+		}
+
+		lastBase = currBase
+		currBase = filepath.Dir(currBase)
+		if lastBase == currBase {
+			return nil, fmt.Errorf("No ksonnet application found")
+		}
+	}
+}
+
+func initManager(rootPath AbsPath, spec ClusterSpec, appFS afero.Fs) (*manager, error) {
+	data, err := spec.data()
+	if err != nil {
+		return nil, err
+	}
+
+	m := newManager(rootPath, appFS)
+
+	if err = m.createAppDirTree(); err != nil {
+		return nil, err
+	}
+
+	if err = m.cacheClusterSpecData(defaultEnvName, data); err != nil {
+		return nil, err
+	}
+
+	return m, nil
+}
+
+func newManager(rootPath AbsPath, appFS afero.Fs) *manager {
+	return &manager{
+		appFS: appFS,
+
+		rootPath:       rootPath,
+		ksonnetPath:    appendToAbsPath(rootPath, ksonnetDir),
+		libPath:        appendToAbsPath(rootPath, libDir),
+		componentsPath: appendToAbsPath(rootPath, componentsDir),
+		vendorDir:      appendToAbsPath(rootPath, vendorDir),
+		schemaDir:      appendToAbsPath(rootPath, schemaDir),
+		vendorLibDir:   appendToAbsPath(rootPath, vendorLibDir),
+	}
+}
+
+func (m *manager) Root() AbsPath {
+	return m.rootPath
+}
+
+func (m *manager) cacheClusterSpecData(name string, specData []byte) error {
+	envPath := string(appendToAbsPath(m.schemaDir, name))
+	err := m.appFS.MkdirAll(envPath, os.ModePerm)
+	if err != nil {
+		return err
+	}
+
+	schemaPath := string(filepath.Join(envPath, schemaFilename))
+	err = afero.WriteFile(m.appFS, schemaPath, specData, os.ModePerm)
+	return err
+}
+
+func (m *manager) createAppDirTree() error {
+	exists, err := afero.DirExists(m.appFS, string(m.rootPath))
+	if err != nil {
+		return fmt.Errorf("Could not check existance of directory '%s':\n%v", m.rootPath, err)
+	} else if exists {
+		return fmt.Errorf("Could not create app; directory '%s' already exists", m.rootPath)
+	}
+
+	paths := []AbsPath{
+		m.rootPath,
+		m.ksonnetPath,
+		m.libPath,
+		m.componentsPath,
+		m.vendorDir,
+		m.schemaDir,
+		m.vendorLibDir,
+	}
+
+	for _, p := range paths {
+		if err := m.appFS.MkdirAll(string(p), os.ModePerm); err != nil {
+			return err
+		}
+	}
+
+	return nil
+}
diff --git a/metadata/manager_test.go b/metadata/manager_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..1329bc8b9d5385d6f5f9d8227bba8a96c540304f
--- /dev/null
+++ b/metadata/manager_test.go
@@ -0,0 +1,133 @@
+package metadata
+
+import (
+	"fmt"
+	"os"
+	"testing"
+
+	"github.com/spf13/afero"
+)
+
+const (
+	blankSwagger     = "/blankSwagger.json"
+	blankSwaggerData = `{}`
+)
+
+var appFS = afero.NewMemMapFs()
+
+func init() {
+	afero.WriteFile(appFS, blankSwagger, []byte(blankSwaggerData), os.ModePerm)
+}
+
+func TestInitSuccess(t *testing.T) {
+	spec, err := ParseClusterSpec(fmt.Sprintf("file:%s", blankSwagger))
+	if err != nil {
+		t.Fatalf("Failed to parse cluster spec: %v", err)
+	}
+
+	appPath := AbsPath("/fromEmptySwagger")
+	_, err = initManager(appPath, spec, appFS)
+	if err != nil {
+		t.Fatalf("Failed to init cluster spec: %v", err)
+	}
+
+	defaultEnvDir := appendToAbsPath(schemaDir, defaultEnvName)
+	paths := []AbsPath{
+		ksonnetDir,
+		libDir,
+		componentsDir,
+		vendorDir,
+		schemaDir,
+		vendorLibDir,
+		defaultEnvDir,
+	}
+
+	for _, p := range paths {
+		path := appendToAbsPath(appPath, string(p))
+		exists, err := afero.DirExists(appFS, string(path))
+		if err != nil {
+			t.Fatalf("Expected to create directory '%s', but failed:\n%v", p, err)
+		} else if !exists {
+			t.Fatalf("Expected to create directory '%s', but failed", path)
+		}
+	}
+
+	envPath := appendToAbsPath(appPath, string(defaultEnvDir))
+	schemaPath := appendToAbsPath(envPath, schemaFilename)
+	bytes, err := afero.ReadFile(appFS, string(schemaPath))
+	if err != nil {
+		t.Fatalf("Failed to read swagger file at '%s':\n%v", schemaPath, err)
+	} else if actualSwagger := string(bytes); actualSwagger != blankSwaggerData {
+		t.Fatalf("Expected swagger file at '%s' to have value: '%s', got: '%s'", schemaPath, blankSwaggerData, actualSwagger)
+	}
+}
+
+func TestFindSuccess(t *testing.T) {
+	findSuccess := func(t *testing.T, appDir, currDir AbsPath) {
+		m, err := findManager(currDir, appFS)
+		if err != nil {
+			t.Fatalf("Failed to find manager at path '%s':\n%v", currDir, err)
+		} else if m.rootPath != appDir {
+			t.Fatalf("Found manager at incorrect path '%s', expected '%s'", m.rootPath, appDir)
+		}
+	}
+
+	spec, err := ParseClusterSpec(fmt.Sprintf("file:%s", blankSwagger))
+	if err != nil {
+		t.Fatalf("Failed to parse cluster spec: %v", err)
+	}
+
+	appPath := AbsPath("/findSuccess")
+	_, err = initManager(appPath, spec, appFS)
+	if err != nil {
+		t.Fatalf("Failed to init cluster spec: %v", err)
+	}
+
+	findSuccess(t, appPath, appPath)
+
+	components := appendToAbsPath(appPath, componentsDir)
+	findSuccess(t, appPath, components)
+
+	// Create empty app file.
+	appFile := appendToAbsPath(components, "app.jsonnet")
+	f, err := appFS.OpenFile(string(appFile), os.O_RDONLY|os.O_CREATE, 0777)
+	if err != nil {
+		t.Fatalf("Failed to touch app file '%s'\n%v", appFile, err)
+	}
+	f.Close()
+
+	findSuccess(t, appPath, appFile)
+}
+
+func TestFindFailure(t *testing.T) {
+	findFailure := func(t *testing.T, currDir AbsPath) {
+		_, err := findManager(currDir, appFS)
+		if err == nil {
+			t.Fatalf("Expected to fail to find ksonnet app in '%s', but succeeded", currDir)
+		}
+	}
+
+	findFailure(t, "/")
+	findFailure(t, "/fakePath")
+	findFailure(t, "")
+}
+
+func TestDoubleNewFailure(t *testing.T) {
+	spec, err := ParseClusterSpec(fmt.Sprintf("file:%s", blankSwagger))
+	if err != nil {
+		t.Fatalf("Failed to parse cluster spec: %v", err)
+	}
+
+	appPath := AbsPath("/doubleNew")
+
+	_, err = initManager(appPath, spec, appFS)
+	if err != nil {
+		t.Fatalf("Failed to init cluster spec: %v", err)
+	}
+
+	targetErr := fmt.Sprintf("Could not create app; directory '%s' already exists", appPath)
+	_, err = initManager(appPath, spec, appFS)
+	if err == nil || err.Error() != targetErr {
+		t.Fatalf("Expected to fail to create app with message '%s', got '%s'", targetErr, err.Error())
+	}
+}