# Shared developer Macs: control agent tool access

A shared developer Mac can support a team, but it cannot safely run autonomous agents if every person and every local process inherits the same pile of credentials. The failure usually starts as convenience: a project token in a shell file, an SSH key loaded once, an editor launched from an old terminal, and an agent asked to "just deploy it." Soon nobody can say which person authorized the action or which process still has access.

Treat identity, credential custody, authorization, and evidence as separate jobs. A macOS login tells you which user owns a process. It does not prove who owns a cloud token, whether an agent should use it, or whether a process that survived lunch should retain access. Teams that blur those jobs get a very quiet form of privilege creep.

## Shared machines need separate identities

A shared workstation is workable only when each human, each credential, and each agent run has a distinct identity that you can inspect and revoke. A machine login is the first boundary, not the whole design.

Do not issue one "dev" account for a group because it seems easier to maintain. That account makes file ownership, shell history, browser sessions, keychain entries, and running processes communal. When a deployment goes wrong, the audit record says only that the shared account did it. That is not accountability. It is a dead end.

Give every developer a separate macOS account and use a separate service identity for unattended work where the work truly needs one. A service identity should have an owner, a stated purpose, a narrow credential set, and a retirement plan. It should not become a disguised team account that people use when their own setup is inconvenient.

The distinction matters most with an agent. A coding agent can launch subprocesses, read its working directory, inspect the environment it inherited, and call approved tools. If two people run agents through the same local account, one person's old environment or helper process may become another person's accidental authority.

Apple Platform Security describes macOS account protections around separate user data and system services. Those protections matter, but they do not decide whether a token copied into a repository, a shared directory, or a process environment is appropriate for the process that finds it. File permissions limit one class of mistake. They do not establish intent.

Use a simple ownership record for every credential that can modify production systems, source control, package publishing, or customer data:

- Name one human owner and one backup contact.
- State the service, allowed operations, and environments.
- Record where the secret is held and how it is rotated.
- Set a review date and a retirement trigger.

Avoid putting the literal secret in the record. The point is to make ownership visible, not to create another copy of a credential.

A named owner does not mean that person must manually perform every action. It means that somebody can answer a direct question: why does this credential exist, who can authorize it, and what breaks if we revoke it now? If nobody can answer, revoke it in a safe window and rebuild access with a clearer purpose.

## A Unix account does not contain a copied secret

macOS account boundaries protect data only while the credential stays inside the account boundary and is not offered through another path. Teams often secure the home directory, then leave the same token in project files, terminal history, editor settings, CI exports, or a long lived agent process.

Start with an inventory that checks the places developers actually use. Do this with permission and care: printing a live secret into a shared terminal scrollback creates the problem you are trying to find.

```sh
# Search filenames and likely configuration references, not secret values.
find "$HOME" -maxdepth 3 \( -name '.env' -o -name '.netrc' -o -name 'credentials*' \) -print

# Show environment variable names for the current shell only.
env | cut -d= -f1 | grep -E 'TOKEN|SECRET|KEY|PASSWORD' || true

# Show loaded SSH identities without printing private key material.
ssh-add -l
```

The last command normally prints one line per identity, with a fingerprint, algorithm, and comment. If it prints `The agent has no identities.`, that is a useful result. If it prints a fingerprint you cannot explain, stop treating the machine as clean and find out which workflow loaded it.

Do not run `env` inside an agent transcript and paste the output into an issue. Agents, terminals, editor logs, and support tickets make poor secret stores. Search for variable names and file locations first. Rotate a credential if you have reason to believe its value entered a transcript or repository history.

A common failed recommendation says, "Put secrets in a local .env file and keep it out of Git." It is popular because it works in five minutes. It also turns every program launched from that directory into a potential credential reader. A `.gitignore` entry prevents a commit. It does not prevent a local agent from reading the file, an archive tool from collecting it, or a developer from copying it into the next project.

Use `.env` files only for low consequence local settings or temporary migration work with a defined cleanup date. For credentials that can change shared systems, keep the value in a protected local store and expose an operation, not the raw value, to the agent.

Process inheritance deserves the same suspicion. A terminal exports `DEPLOY_TOKEN`; an editor starts from that terminal; an extension starts a language server; an agent invokes a tool through the editor. The original developer may have forgotten the token hours ago, but each child process can still read it. macOS permissions did exactly what they were asked to do. The team gave too many processes the secret.

## Credential custody and action authority are different

A credential answers "who can authenticate?" Authorization answers "may this process perform this action now?" Teams often treat possession of the first as proof of the second, especially with API tokens.

Do not hand a bearer token to an agent merely because the agent needs one API call. A bearer token acts like the holder for the scope it carries. Once the token appears in an agent's context, a tool argument, a child process environment, or debug output, you must assume every system that can read that material may reuse it.

Instead, define an action contract. The agent requests a named operation with structured inputs. A trusted local component holds the credential, validates the allowed destination and request shape, performs the call, and returns the result. The agent never receives an empty placeholder that it can later fill from some other source. It receives no credential at all.

For example, a release agent may need to create a deployment record. Its request can look like this:

```json
{
  "action": "create_deployment",
  "environment": "staging",
  "revision": "7c31f4a",
  "summary": "Fix request timeout handling"
}
```

The trusted component can map `create_deployment` to one approved endpoint and one stored credential. It should reject an input that tries to supply a host, path, authorization header, or arbitrary request body. If the action contract allows arbitrary URLs and headers, it has recreated general network access with more ceremony.

Keep the credential scope small enough that a mistake has a bounded result. Separate read access from write access. Separate staging from production. Prefer short lived credentials when the upstream service supports them, but do not treat short life as a cure for broad scope. A token that lives briefly can still make an irreversible call in its first second.

The same distinction applies to SSH. An SSH private key proves control of a principal. It does not explain why a local agent should run a command on a host at this moment. Put command and host selection into the authorization decision. Do not give an agent a generic shell pathway and hope repository instructions will restrain it.

## Local processes can borrow more authority than you intended

A local process can inherit authority through environment variables, open file descriptors, Unix sockets, browser sessions, and helper services. The risky process is often not malicious. It is stale, misconfigured, or launched by someone who did not realize what its parent had already granted.

Inspect a suspicious process before you kill it. On macOS, these commands give you useful clues without exposing credential values:

```sh
ps -axo pid,ppid,user,command | grep -i '[a]gent\|[c]laude\|[n]ode\|[p]ython'
lsof -nP -p <PID> | grep -E 'cwd|unix|TCP|IPv4|IPv6'
```

The first command shows the process ID, parent process ID, account, and launch command. The second commonly shows the current working directory, Unix socket paths, and open network connections. A working directory inside an old project and a parent terminal from a former session explain more incidents than exotic malware does.

Do not assume a process ended because its window disappeared. Editors keep language servers running. Terminal multiplexers preserve shells. Build tools start watchers. A local MCP client may maintain a connection long after the developer has stopped paying attention.

Make ending access a deliberate action. Quit the agent client, stop the terminal session that launched it, and remove any temporary authorization granted for that run. If a helper must stay running, record its purpose and make its process identity visible to the person approving access.

A practical handoff test catches problems early. Developer A runs an agent with access to a staging action, then closes the agent and signs out. Developer B signs into their own account and runs a harmless request. B should not find A's project directory, environment variables, SSH socket, browser session, or active authorization. If B can reach any of them, the team has shared state that needs removal.

Do not solve this by making developers administrators. Administrator rights can be necessary for system management, but they widen the blast radius of a careless installer or local script. Use a standard account for daily agent work unless a specific task requires elevation, then elevate for that task and return to normal use.

## SSH agents need the same scrutiny as private keys

An SSH agent holds signing authority behind a local socket, so a process that can reach that socket may request signatures without reading the private key file. This is better than scattering private keys across disk, but it is not a free pass for every process on the workstation.

OpenSSH documents `SSH_AUTH_SOCK` as the path to the agent socket. Treat that environment variable as sensitive routing information. If an agent inherits it, the agent may be able to ask for signatures from identities currently loaded into the SSH agent. The private key remains hidden, but the operational risk remains.

Run this before using automation against a remote system:

```sh
printf '%s\n' "${SSH_AUTH_SOCK:-SSH_AUTH_SOCK is unset}"
ssh-add -l
ssh -G deploy@staging.example.internal | grep -E '^(hostname|user|identityfile|forwardagent) '
```

`ssh -G` prints the effective OpenSSH client configuration. The output shape includes lines such as `user deploy`, `identityfile ...`, and `forwardagent no`. Check the last line carefully. Agent forwarding lets a remote host use your local agent through the forwarded connection. It should stay off unless you can explain the exact remote hop and why it needs it.

The OpenSSH `ssh_config` manual describes `ForwardAgent` and warns that forwarding can expose the local agent to users with sufficient access on the remote host. Teams still enable it broadly because it avoids copying keys and makes a jump host feel convenient. The convenience is real. So is the fact that a compromised or overly permissive remote environment can request signatures while the forwarded session exists.

Use separate identities for separate access classes. A production deployment identity should not sit beside a personal source control identity in one agent simply because both are useful during development. Remove identities after the task when practical:

```sh
ssh-add -d ~/.ssh/id_staging_deploy
# Or clear every identity after a short, dedicated session.
ssh-add -D
```

Clearing all identities can disrupt other legitimate work, so use it at the end of a dedicated session, not in a shared shell tab that somebody else depends on. The better pattern is to avoid shared shells entirely.

Avoid putting private key paths, host aliases, and permissive forwarding rules in repository configuration that every developer blindly adopts. A repository can document the expected host and account. Each developer should decide which local identity may reach it.

## Approval must name the process and the action

An approval prompt helps only when a human can identify the caller, understand the requested operation, and revoke the permission after the work ends. A prompt that says "allow access" trains people to approve noise.

Per session approval suits ordinary development work. The first call from a newly launched agent process shows the caller identity and the user decides whether that run may use an action channel. The authorization should end when the process exits, not survive as an invisible preference.

Per call approval suits actions with asymmetric consequences: production deletion, package publication, changing a payment setting, or running a command on a sensitive host. It adds friction by design. Do not apply it to a read only status call merely to create a comforting ritual; people will approve it without reading it.

The approval card should answer these questions in plain language:

- Which local process requested access, including its signing authority when available?
- Which stored credential or action category will it use?
- Which destination, host, or environment will receive the request?
- What operation will occur and what inputs materially change its effect?
- Does the approval last for this call or only until this process exits?

Do not accept an approval system that identifies the caller only as "terminal" or "agent." A local machine can run multiple terminals and multiple agent processes. The person approving needs enough information to distinguish a newly started work run from an old process that happens to be alive.

Sallyport uses a fixed decision ladder: a locked vault denies every action, a new agent process requires session authorization by default, and selected credentials can require approval on every use. That narrow model is preferable to an elaborate local policy language on a developer laptop, where obscure rules age badly and nobody can confidently predict which one won.

Approval is not a substitute for scope. A human may approve the wrong thing. Constrain destinations, credentials, and action shapes first, then ask for approval when human judgment adds useful control.

## A log must answer ownership questions after the fact

A useful audit record lets an engineer reconstruct who ran what, which local process received authority, what external action occurred, and when somebody withdrew access. A terminal scrollback cannot meet that standard because users edit it, shells rotate it, and separate processes disappear into one history file.

Keep two views of the same event stream. One view tracks sessions: agent process identity, start and end time, approvals, and revocation. The other tracks actions: timestamp, destination, method or command, credential reference, result, and error. Join them through a session identifier, but keep the information understandable without a database archaeology project.

Tamper evidence changes the quality of the conversation after an incident. A write once sequence with chained hashes makes silent edits detectable. It does not prove that every recorded action was wise, and it does not revive records you failed to collect. It does make "someone cleaned up the log" easier to challenge.

Verify the audit trail independently. Sallyport projects its session and activity journals from an encrypted hash chained audit log, and `sp audit verify` checks the chain offline without access to vault keys. That separation matters because the verification command should not need the very secret store whose use you are investigating.

Logs must protect secrets too. Record a credential reference, action name, destination, and outcome. Do not record bearer tokens, request authorization headers, private key material, or command arguments that embed passwords. A complete log that reproduces every secret is a second credential vault with worse access control.

Review failed actions, not only successful ones. Repeated denials may reveal an agent trying an obsolete action contract, a developer using the wrong account, or a background process that outlived its task. A single unexplained successful action deserves attention, but failures show the edges where your controls do not match real work.

## A workable operating model for a team Mac

Teams can run local agents safely enough for serious work when they make shared hardware less important than individual identity and explicit action authority. The operating model needs daily habits, not a security document that appears only during onboarding.

Use this sequence when you add an agent workflow to a shared office machine:

1. Assign the human owner, allowed service, environment, and retirement condition for each credential.
2. Create a dedicated action contract for the agent instead of exposing a generic token or shell session.
3. Launch the agent from the intended developer account with an empty or minimal credential environment.
4. Approve the new process only after checking its identity and requested destination.
5. End the run, revoke it if needed, and confirm that its helper processes and SSH identities are gone.

This sequence is deliberately less convenient than putting one production token in a popular shell profile. That shortcut feels efficient until a contractor uses the machine, an editor inherits the environment, or an old process remains active through the next person's shift.

Define emergency access before an outage. A team needs a documented person who can approve a break glass action, a separate credential with narrow emergency scope, and a log record that explains why it was used. Do not leave a powerful permanent token in a shared folder "for emergencies." It will become ordinary access because it is there.

For work that requires a local action gateway, keep the gateway on the developer's own account and make the process boundary visible. A shared Mac can host several users over time; it should not host one undifferentiated pool of authority.

## Test revocation while the agent is still running

Revocation is credible only if it blocks the next action from a process that already had permission. Testing it after you quit every client proves very little.

Set up a harmless staging action that returns a known response. Launch an agent process, approve it for the session, and perform the action once. Then revoke the session or lock the credential store while the process remains alive. Request the same action again.

The expected result is a denial tied to the active process or locked state. The agent should not fall back to a token in its environment, an existing SSH socket, or a cached browser session. If it succeeds, inspect the path it used before you add more prompts or policies.

Run the same test after a developer signs out and another signs in. Then test it after the machine sleeps and wakes, because local helper connections and SSH agents sometimes expose assumptions that only appear across a long workday. Keep a short record of the results, including the exact process identity and the action you tested.

The most useful first change is not a larger secret manager or a longer approval checklist. Remove one broad credential from an agent's reach, replace it with a narrow action path, and prove that revocation stops a still running process. That test tells you whether the team controls access or merely documents it.
